@legenki/print2medusa 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (28) hide show
  1. package/.medusa/server/examples/basic-store/medusa-config.snippet.js +57 -0
  2. package/.medusa/server/src/admin/index.js +133 -0
  3. package/.medusa/server/src/admin/index.mjs +134 -0
  4. package/.medusa/server/src/api/admin/printful/status/route.js +15 -0
  5. package/.medusa/server/src/api/admin/printful/sync/route.js +25 -0
  6. package/.medusa/server/src/jobs/sync-products.js +31 -0
  7. package/.medusa/server/src/modules/printful/index.js +13 -0
  8. package/.medusa/server/src/modules/printful/migrations/Migration20260726000000.js +103 -0
  9. package/.medusa/server/src/modules/printful/models/printful-order-link.js +12 -0
  10. package/.medusa/server/src/modules/printful/models/printful-product-link.js +13 -0
  11. package/.medusa/server/src/modules/printful/models/printful-sync-log.js +15 -0
  12. package/.medusa/server/src/modules/printful/models/printful-variant-link.js +13 -0
  13. package/.medusa/server/src/modules/printful/service.js +126 -0
  14. package/.medusa/server/src/providers/printful-fulfillment/index.js +11 -0
  15. package/.medusa/server/src/providers/printful-fulfillment/service.js +97 -0
  16. package/.medusa/server/src/subscribers/order-placed.js +37 -0
  17. package/.medusa/server/src/subscribers/payment-captured.js +61 -0
  18. package/.medusa/server/src/utils/errors.js +24 -0
  19. package/.medusa/server/src/utils/index.js +21 -0
  20. package/.medusa/server/src/utils/mappers.js +250 -0
  21. package/.medusa/server/src/utils/printful-client.js +169 -0
  22. package/.medusa/server/src/utils/types.js +3 -0
  23. package/.medusa/server/src/workflows/create-printful-order.js +123 -0
  24. package/.medusa/server/src/workflows/index.js +13 -0
  25. package/.medusa/server/src/workflows/sync-products.js +227 -0
  26. package/LICENSE +21 -0
  27. package/README.md +127 -0
  28. package/package.json +87 -0
@@ -0,0 +1,227 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.syncProductsWorkflow = void 0;
4
+ const workflows_sdk_1 = require("@medusajs/framework/workflows-sdk");
5
+ const core_flows_1 = require("@medusajs/medusa/core-flows");
6
+ const utils_1 = require("@medusajs/framework/utils");
7
+ const printful_1 = require("../modules/printful");
8
+ const mappers_1 = require("../utils/mappers");
9
+ const createSyncLogStep = (0, workflows_sdk_1.createStep)("printful-create-sync-log", async (_input, { container }) => {
10
+ const printful = container.resolve(printful_1.PRINTFUL_MODULE);
11
+ const log = await printful.createPrintfulSyncLogs({
12
+ status: "running",
13
+ started_at: new Date(),
14
+ products_created: 0,
15
+ products_updated: 0,
16
+ products_failed: 0,
17
+ });
18
+ return new workflows_sdk_1.StepResponse(log);
19
+ });
20
+ const syncProductsStep = (0, workflows_sdk_1.createStep)("printful-sync-products", async (input, { container }) => {
21
+ const printful = container.resolve(printful_1.PRINTFUL_MODULE);
22
+ const productModule = container.resolve(utils_1.Modules.PRODUCT);
23
+ const client = await printful.getClient();
24
+ const options = await printful.getOptions();
25
+ const storeId = await printful.getStoreId();
26
+ const summaries = await client.listAllSyncProducts({
27
+ limit: 100,
28
+ });
29
+ const toProcess = input.limit != null ? summaries.slice(0, input.limit) : summaries;
30
+ const counters = {
31
+ created: 0,
32
+ updated: 0,
33
+ failed: 0,
34
+ errors: [],
35
+ };
36
+ for (const summary of toProcess) {
37
+ if (summary.is_ignored) {
38
+ continue;
39
+ }
40
+ try {
41
+ const detail = await client.getSyncProduct(summary.id);
42
+ if (!detail.sync_variants?.length) {
43
+ counters.failed += 1;
44
+ counters.errors.push(`Product ${summary.id} has no variants`);
45
+ continue;
46
+ }
47
+ const mapped = (0, mappers_1.mapSyncProductToMedusa)(detail, {
48
+ storeId: options.storeId,
49
+ defaultCurrency: options.defaultCurrency,
50
+ markupPercent: options.markupPercent,
51
+ });
52
+ const existingLink = await printful.findProductLink(String(summary.id));
53
+ if (existingLink?.medusa_product_id) {
54
+ const productId = existingLink.medusa_product_id;
55
+ // Update core product fields.
56
+ await productModule.updateProducts(productId, {
57
+ title: mapped.title,
58
+ thumbnail: mapped.thumbnail,
59
+ metadata: mapped.metadata,
60
+ status: mapped.status,
61
+ });
62
+ // Upsert variants so price/assortment changes in Printful reach Medusa.
63
+ const product = await productModule.retrieveProduct(productId, {
64
+ relations: ["variants"],
65
+ });
66
+ const { toCreate, toUpdate } = (0, mappers_1.diffVariantsForUpsert)(mapped.variants, (product.variants ?? []).map((pv) => ({
67
+ id: pv.id,
68
+ metadata: pv.metadata,
69
+ })));
70
+ if (toUpdate.length) {
71
+ await (0, core_flows_1.batchProductVariantsWorkflow)(container).run({
72
+ input: {
73
+ update: toUpdate.map((u) => ({
74
+ id: u.id,
75
+ title: u.title,
76
+ prices: u.prices,
77
+ metadata: u.metadata,
78
+ })),
79
+ },
80
+ });
81
+ }
82
+ if (toCreate.length) {
83
+ const { result: created } = await (0, core_flows_1.batchProductVariantsWorkflow)(container).run({
84
+ input: {
85
+ create: toCreate.map((c) => ({
86
+ product_id: productId,
87
+ title: c.title,
88
+ sku: c.sku,
89
+ options: c.options,
90
+ prices: c.prices,
91
+ metadata: c.metadata,
92
+ manage_inventory: c.manage_inventory,
93
+ allow_backorder: c.allow_backorder,
94
+ })),
95
+ },
96
+ });
97
+ for (const variant of created.created ?? []) {
98
+ const syncVariantId = variant.metadata?.printful_sync_variant_id;
99
+ if (!syncVariantId) {
100
+ continue;
101
+ }
102
+ await printful.createPrintfulVariantLinks({
103
+ printful_store_id: storeId,
104
+ printful_sync_product_id: String(summary.id),
105
+ printful_sync_variant_id: String(syncVariantId),
106
+ medusa_variant_id: variant.id,
107
+ last_synced_at: new Date(),
108
+ });
109
+ }
110
+ }
111
+ // Refresh timestamps on existing variant links.
112
+ for (const v of detail.sync_variants) {
113
+ const existingVariant = await printful.findVariantLink(String(v.id));
114
+ if (existingVariant) {
115
+ await printful.updatePrintfulVariantLinks({
116
+ id: existingVariant.id,
117
+ last_synced_at: new Date(),
118
+ });
119
+ }
120
+ }
121
+ await printful.updatePrintfulProductLinks({
122
+ id: existingLink.id,
123
+ last_synced_at: new Date(),
124
+ });
125
+ counters.updated += 1;
126
+ }
127
+ else {
128
+ // Ensure unique handle if collision
129
+ let handle = mapped.handle;
130
+ try {
131
+ const existing = await productModule.listProducts({ handle: [handle] }, { take: 1 });
132
+ if (existing.length) {
133
+ handle = `${handle}-pf-${summary.id}`;
134
+ }
135
+ }
136
+ catch {
137
+ // ignore lookup errors
138
+ }
139
+ const { result } = await (0, core_flows_1.createProductsWorkflow)(container).run({
140
+ input: {
141
+ products: [
142
+ {
143
+ title: mapped.title,
144
+ handle,
145
+ status: mapped.status,
146
+ thumbnail: mapped.thumbnail,
147
+ images: mapped.images,
148
+ options: mapped.options,
149
+ variants: mapped.variants.map((v) => ({
150
+ title: v.title,
151
+ sku: v.sku,
152
+ options: v.options,
153
+ prices: v.prices,
154
+ metadata: v.metadata,
155
+ manage_inventory: v.manage_inventory,
156
+ allow_backorder: v.allow_backorder,
157
+ })),
158
+ metadata: mapped.metadata,
159
+ external_id: mapped.external_id,
160
+ },
161
+ ],
162
+ },
163
+ });
164
+ const created = result[0];
165
+ await printful.createPrintfulProductLinks({
166
+ printful_store_id: storeId,
167
+ printful_sync_product_id: String(summary.id),
168
+ medusa_product_id: created.id,
169
+ last_synced_at: new Date(),
170
+ });
171
+ for (const variant of created.variants ?? []) {
172
+ const syncVariantId = variant.metadata?.printful_sync_variant_id;
173
+ if (!syncVariantId) {
174
+ continue;
175
+ }
176
+ await printful.createPrintfulVariantLinks({
177
+ printful_store_id: storeId,
178
+ printful_sync_product_id: String(summary.id),
179
+ printful_sync_variant_id: String(syncVariantId),
180
+ medusa_variant_id: variant.id,
181
+ last_synced_at: new Date(),
182
+ });
183
+ }
184
+ counters.created += 1;
185
+ }
186
+ }
187
+ catch (err) {
188
+ counters.failed += 1;
189
+ const message = err instanceof Error ? err.message : String(err);
190
+ counters.errors.push(`Product ${summary.id}: ${message}`);
191
+ }
192
+ }
193
+ return new workflows_sdk_1.StepResponse(counters);
194
+ });
195
+ const finalizeSyncLogStep = (0, workflows_sdk_1.createStep)("printful-finalize-sync-log", async (input, { container }) => {
196
+ const printful = container.resolve(printful_1.PRINTFUL_MODULE);
197
+ const failedHard = input.counters.failed > 0 &&
198
+ input.counters.created === 0 &&
199
+ input.counters.updated === 0;
200
+ const log = await printful.updatePrintfulSyncLogs({
201
+ id: input.logId,
202
+ status: failedHard ? "failed" : "success",
203
+ finished_at: new Date(),
204
+ products_created: input.counters.created,
205
+ products_updated: input.counters.updated,
206
+ products_failed: input.counters.failed,
207
+ error_message: input.counters.errors.length > 0
208
+ ? input.counters.errors.slice(0, 20).join("\n")
209
+ : null,
210
+ });
211
+ return new workflows_sdk_1.StepResponse(log);
212
+ });
213
+ exports.syncProductsWorkflow = (0, workflows_sdk_1.createWorkflow)("printful-sync-products", (input) => {
214
+ const log = createSyncLogStep();
215
+ const counters = syncProductsStep(input);
216
+ const finalizeInput = (0, workflows_sdk_1.transform)({ log, counters }, (data) => ({
217
+ logId: data.log.id,
218
+ counters: data.counters,
219
+ }));
220
+ const finalLog = finalizeSyncLogStep(finalizeInput);
221
+ return new workflows_sdk_1.WorkflowResponse({
222
+ sync_log: finalLog,
223
+ counters,
224
+ });
225
+ });
226
+ exports.default = exports.syncProductsWorkflow;
227
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic3luYy1wcm9kdWN0cy5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uLy4uLy4uL3NyYy93b3JrZmxvd3Mvc3luYy1wcm9kdWN0cy50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOzs7QUFBQSxxRUFNMEM7QUFDMUMsNERBR29DO0FBQ3BDLHFEQUFtRDtBQUNuRCxrREFBcUQ7QUFFckQsOENBQWdGO0FBY2hGLE1BQU0saUJBQWlCLEdBQUcsSUFBQSwwQkFBVSxFQUNsQywwQkFBMEIsRUFDMUIsS0FBSyxFQUFFLE1BQVksRUFBRSxFQUFFLFNBQVMsRUFBRSxFQUFFLEVBQUU7SUFDcEMsTUFBTSxRQUFRLEdBQTBCLFNBQVMsQ0FBQyxPQUFPLENBQUMsMEJBQWUsQ0FBQyxDQUFBO0lBQzFFLE1BQU0sR0FBRyxHQUFHLE1BQU0sUUFBUSxDQUFDLHNCQUFzQixDQUFDO1FBQ2hELE1BQU0sRUFBRSxTQUFTO1FBQ2pCLFVBQVUsRUFBRSxJQUFJLElBQUksRUFBRTtRQUN0QixnQkFBZ0IsRUFBRSxDQUFDO1FBQ25CLGdCQUFnQixFQUFFLENBQUM7UUFDbkIsZUFBZSxFQUFFLENBQUM7S0FDbkIsQ0FBQyxDQUFBO0lBQ0YsT0FBTyxJQUFJLDRCQUFZLENBQUMsR0FBRyxDQUFDLENBQUE7QUFDOUIsQ0FBQyxDQUNGLENBQUE7QUFFRCxNQUFNLGdCQUFnQixHQUFHLElBQUEsMEJBQVUsRUFDakMsd0JBQXdCLEVBQ3hCLEtBQUssRUFBRSxLQUF3QixFQUFFLEVBQUUsU0FBUyxFQUFFLEVBQUUsRUFBRTtJQUNoRCxNQUFNLFFBQVEsR0FBMEIsU0FBUyxDQUFDLE9BQU8sQ0FBQywwQkFBZSxDQUFDLENBQUE7SUFDMUUsTUFBTSxhQUFhLEdBQUcsU0FBUyxDQUFDLE9BQU8sQ0FBQyxlQUFPLENBQUMsT0FBTyxDQUFDLENBQUE7SUFDeEQsTUFBTSxNQUFNLEdBQUcsTUFBTSxRQUFRLENBQUMsU0FBUyxFQUFFLENBQUE7SUFDekMsTUFBTSxPQUFPLEdBQUcsTUFB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
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Andy Legenki
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,127 @@
1
+ # @legenki/print2medusa
2
+
3
+ Printful → Medusa v2 plugin: **sync Store Products**, **auto-create Printful orders** on payment capture, and a **Fulfillment Provider** for admin shipping options.
4
+
5
+ MIT licensed.
6
+
7
+ ## Requirements
8
+
9
+ - Node.js ≥ 20
10
+ - Medusa **2.18.x** (peer dependency)
11
+ - Printful store on the **Manual order / API** platform with a private token (`orders`, `sync_products` scopes)
12
+
13
+ ## Install
14
+
15
+ ```bash
16
+ npm install @legenki/print2medusa
17
+ ```
18
+
19
+ Register the plugin and fulfillment provider in `medusa-config.ts`:
20
+
21
+ ```ts
22
+ plugins: [
23
+ {
24
+ resolve: "@legenki/print2medusa",
25
+ options: {
26
+ apiToken: process.env.PRINTFUL_API_TOKEN,
27
+ storeId: process.env.PRINTFUL_STORE_ID, // required for account-level tokens
28
+ // autoSubmitOrders: true,
29
+ // createOnOrderPlaced: false,
30
+ // allowPartialOrders: false,
31
+ // markupPercent: 30,
32
+ // defaultCurrency: "USD",
33
+ },
34
+ },
35
+ ],
36
+ modules: [
37
+ {
38
+ resolve: "@medusajs/medusa/fulfillment",
39
+ options: {
40
+ providers: [
41
+ {
42
+ resolve: "@medusajs/medusa/fulfillment-manual",
43
+ id: "manual",
44
+ },
45
+ {
46
+ resolve: "@legenki/print2medusa/providers/printful-fulfillment",
47
+ id: "printful",
48
+ options: {
49
+ apiToken: process.env.PRINTFUL_API_TOKEN,
50
+ storeId: process.env.PRINTFUL_STORE_ID,
51
+ },
52
+ },
53
+ ],
54
+ },
55
+ },
56
+ ],
57
+ ```
58
+
59
+ Then migrate:
60
+
61
+ ```bash
62
+ npx medusa db:migrate
63
+ ```
64
+
65
+ See `examples/basic-store/` for a fuller snippet.
66
+
67
+ ## What it does (MVP)
68
+
69
+ | Feature | How |
70
+ |--------|-----|
71
+ | Product sync | Admin **Sync Now** or `POST /admin/printful/sync` → workflow pulls Printful Sync Products into Medusa |
72
+ | Links | `printful_product_link` / `printful_variant_link` (+ metadata IDs) |
73
+ | Orders | On `payment.captured` → creates Printful order with **`sync_variant_id`** |
74
+ | Fulfillment provider | Select Printful shipping option in Admin locations |
75
+ | Status | `GET /admin/printful/status` + product list widget |
76
+
77
+ ### Idempotency
78
+
79
+ - Re-sync updates existing products via link tables (no duplicates) and **upserts variants** — price and assortment changes in Printful reach Medusa; manually-added Medusa variants are left untouched.
80
+ - Concurrent / re-fired payment events will not create a second Printful order: the order is **claimed insert-first** via a unique index on `printful_order_link.medusa_order_id` before the Printful API is called.
81
+ - Shipping `province` is normalized to the 2-letter `state_code` Printful expects for US/CA.
82
+
83
+ ## Admin usage
84
+
85
+ 1. Create products in Printful (Store Products).
86
+ 2. Open Medusa Admin → Products list → **Printful → Sync Now**.
87
+ 3. Configure a location shipping option using the **Printful** fulfillment provider.
88
+ 4. Place a test order and capture payment → Printful receives the order.
89
+
90
+ ## Local plugin development
91
+
92
+ ```bash
93
+ npm install
94
+ npm run build
95
+ npm run dev # watch + yalc publish
96
+ npm test
97
+ npm run typecheck
98
+ ```
99
+
100
+ In a host Medusa app:
101
+
102
+ ```bash
103
+ npx medusa plugin:add @legenki/print2medusa
104
+ ```
105
+
106
+ ## Architecture notes
107
+
108
+ - **Printful is source of truth** for products; Medusa holds a copy + links.
109
+ - Printful API **v1** (`https://api.printful.com`).
110
+ - Long-running sync runs as a Medusa **workflow** (not a blocking HTTP body only—route awaits the workflow today; can be queued later).
111
+ - Webhooks / multi-store / live rates: planned Phase 2.
112
+
113
+ ## Options
114
+
115
+ | Option | Description |
116
+ |--------|-------------|
117
+ | `apiToken` | Printful private token (required) |
118
+ | `storeId` | `X-PF-Store-Id` for account-level tokens |
119
+ | `autoSubmitOrders` | Confirm orders for fulfillment (default true) |
120
+ | `createOnOrderPlaced` | Also create Printful order on `order.placed` |
121
+ | `allowPartialOrders` | Allow orders that mix Printful + non-Printful items |
122
+ | `markupPercent` | Markup on retail prices during sync |
123
+ | `defaultCurrency` | Fallback currency code |
124
+
125
+ ## License
126
+
127
+ MIT © Andy Legenki
package/package.json ADDED
@@ -0,0 +1,87 @@
1
+ {
2
+ "name": "@legenki/print2medusa",
3
+ "version": "0.1.0",
4
+ "description": "Printful → Medusa v2 plugin: product sync, auto fulfillment, and admin tools",
5
+ "author": "Andy Legenki",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/legenki/print2medusa.git"
10
+ },
11
+ "files": [
12
+ ".medusa/server"
13
+ ],
14
+ "publishConfig": {
15
+ "access": "public"
16
+ },
17
+ "exports": {
18
+ "./package.json": "./package.json",
19
+ "./workflows": "./.medusa/server/src/workflows/index.js",
20
+ "./.medusa/server/src/modules/*": "./.medusa/server/src/modules/*/index.js",
21
+ "./modules/*": "./.medusa/server/src/modules/*/index.js",
22
+ "./providers/*": "./.medusa/server/src/providers/*/index.js",
23
+ "./*": "./.medusa/server/src/*.js",
24
+ "./admin": {
25
+ "import": "./.medusa/server/src/admin/index.mjs",
26
+ "require": "./.medusa/server/src/admin/index.js",
27
+ "default": "./.medusa/server/src/admin/index.js"
28
+ }
29
+ },
30
+ "keywords": [
31
+ "medusa",
32
+ "medusa-v2",
33
+ "medusa-plugin",
34
+ "medusa-plugin-integration",
35
+ "medusa-plugin-shipping",
36
+ "medusa-plugin-other",
37
+ "printful",
38
+ "print-on-demand",
39
+ "fulfillment"
40
+ ],
41
+ "scripts": {
42
+ "build": "medusa plugin:build",
43
+ "dev": "medusa plugin:develop",
44
+ "lint": "medusa lint",
45
+ "typecheck": "tsc --noEmit",
46
+ "test": "vitest run",
47
+ "test:watch": "vitest",
48
+ "prepublishOnly": "medusa plugin:build"
49
+ },
50
+ "devDependencies": {
51
+ "@medusajs/admin-sdk": "2.18.0",
52
+ "@medusajs/cli": "2.18.0",
53
+ "@medusajs/eslint-plugin": "2.18.0",
54
+ "@medusajs/framework": "2.18.0",
55
+ "@medusajs/icons": "2.18.0",
56
+ "@medusajs/medusa": "2.18.0",
57
+ "@medusajs/test-utils": "2.18.0",
58
+ "@medusajs/ui": "4.2.0",
59
+ "@swc/core": "^1.7.28",
60
+ "@types/node": "^20.0.0",
61
+ "@types/react": "^18.3.2",
62
+ "@types/react-dom": "^18.2.25",
63
+ "eslint": "^9.0.0",
64
+ "jiti": "^2.0.0",
65
+ "prop-types": "^15.8.1",
66
+ "react": "^18.2.0",
67
+ "react-dom": "^18.2.0",
68
+ "ts-node": "^10.9.2",
69
+ "typescript": "^5.6.2",
70
+ "vite": "^5.2.11",
71
+ "vitest": "^3.0.0",
72
+ "yalc": "^1.0.0-pre.53"
73
+ },
74
+ "peerDependencies": {
75
+ "@medusajs/admin-sdk": "2.18.0",
76
+ "@medusajs/cli": "2.18.0",
77
+ "@medusajs/framework": "2.18.0",
78
+ "@medusajs/icons": "2.18.0",
79
+ "@medusajs/medusa": "2.18.0",
80
+ "@medusajs/test-utils": "2.18.0",
81
+ "@medusajs/ui": "4.2.0"
82
+ },
83
+ "engines": {
84
+ "node": ">=20"
85
+ },
86
+ "packageManager": "npm@11.17.0"
87
+ }