medusa-plugin-print-trail 1.0.0 → 1.0.1
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.
- package/.medusa/server/src/admin/index.js +2 -2
- package/.medusa/server/src/admin/index.mjs +2 -2
- package/.medusa/server/src/config.js +3 -3
- package/CHANGELOG.md +9 -0
- package/README.md +46 -486
- package/package.json +1 -1
|
@@ -92,7 +92,7 @@ function formatPrice(amount, currency) {
|
|
|
92
92
|
}
|
|
93
93
|
}
|
|
94
94
|
const ENV_SNIPPET = [
|
|
95
|
-
"PRINT_TRAIL_BASE_URL=https://printtrail
|
|
95
|
+
"PRINT_TRAIL_BASE_URL=https://api.printtrail.com",
|
|
96
96
|
"PRINT_TRAIL_CHANNEL_ID=",
|
|
97
97
|
"PRINT_TRAIL_API_TOKEN=",
|
|
98
98
|
"MEDUSA_BACKEND_URL=https://",
|
|
@@ -1467,7 +1467,7 @@ const diagnosis = { "not-configured": "Not connected: the Print Trail API creden
|
|
|
1467
1467
|
const webhook = { "states": { "registered": "Registered here", "not-registered": "Not registered", "elsewhere": "Sending elsewhere", "unknown": "Unknown (couldn't read the channel)", "no-backend-url": "No backend URL (MEDUSA_BACKEND_URL isn't set)" }, "actions": { "registered": "Webhook registered.", "already-registered": "The webhook is already registered here.", "needs-confirmation": "The webhook wasn't changed.", "cannot-register": "The webhook wasn't registered: the channel couldn't be read.", "invalid-url": "The webhook wasn't registered: Print Trail never accepts this address. See the notes under Webhook.", "failed": "Print Trail didn't register the webhook: {{detail}}", "busy": "Another Print Trail connection check is running. Try again in a moment." }, "confirmTitle": "Register this address?", "confirmUnsafe": "Print Trail only sends webhooks to https:// addresses on a public host, and refuses to register any other. {{url}} isn't one, so Print Trail will refuse it unless it's a development Print Trail or the bundled mock. For a local store, use an HTTPS tunnel and set MEDUSA_BACKEND_URL to its address. Register it anyway?", "confirmYes": "Register anyway", "confirmElsewhereTitle": "Send updates to this store?", "confirmElsewhere": "Print Trail is currently sending this channel's updates to {{url}}. Send them to this store instead?", "confirmElsewhereYes": "Send them here" };
|
|
1468
1468
|
const warnings = { "not-configured": "Print Trail isn't connected yet.", "no-webhook-secret": "No webhook secret yet, so every webhook is refused. Press Test connection to fetch it.", "webhook-secret-mismatch": "PRINT_TRAIL_WEBHOOK_SECRET differs from the secret the Print Trail API returned. The plugin uses Print Trail's; remove the variable or update it.", "no-backend-url": "MEDUSA_BACKEND_URL isn't set, so the webhook can't be registered.", "no-admin-url": "MEDUSA_ADMIN_URL isn't set, so Print Trail orders carry no link back to Medusa.", "no-admin-email": "No admin email is set (PRINT_TRAIL_ADMIN_EMAIL), so nobody is emailed when a cancel comes too late.", "in-memory-locking": "Locks are per process. If this store runs more than one server or worker process, configure Medusa's Redis locking provider (@medusajs/medusa/locking-redis), or two processes can send the same order at once.", "postgres-locking": "The Postgres locking provider is the default, and the plugin doesn't support it: it never reclaims an expired lock. Use @medusajs/medusa/locking-redis.", "postgres-locking-configured": "A Postgres locking provider is configured. Don't make it the default: the plugin doesn't support it.", "no-print-trail-shipping-options": "No shipping option uses Print Trail yet, so no order is sent.", "not-https": "Print Trail only sends webhooks to https:// addresses, and refuses to register any other.", "local-address": "This is a local, private or reserved address. Print Trail can't reach it and refuses to register it; for a local store, use an HTTPS tunnel.", "too-long": "This address is longer than 200 characters, which Print Trail never accepts.", "credentials": "This address has a user name or password, which Print Trail never accepts.", "fragment": "This address has a #fragment, which Print Trail never accepts.", "undeliverable": "This address is registered, but Print Trail API 1.2.0 and later deliver nothing to it. Register an https:// address on a public host (an HTTPS tunnel for a local store)." };
|
|
1469
1469
|
const health = { "reconnect": "Reconnect Print Trail", "reconnectBody": "Print Trail refused the connection: the API token or channel ID is wrong. Check PRINT_TRAIL_API_TOKEN and PRINT_TRAIL_CHANNEL_ID, restart Medusa and press Test connection. Orders, cancels and edits waiting meanwhile are retried automatically once it works.", "channelInactive": "The Print Trail channel is inactive", "channelInactiveBody": "Print Trail refuses every request from this channel and sends it no webhooks, so orders aren't sent. Make the channel active in Print Trail (Settings → Channels), then press Test connection. Orders, cancels and edits waiting meanwhile are retried automatically once it is active.", "unmatched": "{{count}} SKUs aren't known to Print Trail.", "unmatchedLink": "See SKU health" };
|
|
1470
|
-
const shipping = { "title": "Print Trail shipping", "description": "Calculated shipping options priced by weight band. Each option ships with one Print Trail method in one currency.", "back": "Print Trail settings", "create": "Create", "createTitle": "New Print Trail shipping option", "editTitle": "Edit Print Trail shipping option", "deleteTitle": "Delete {{name}}?", "deleteBody": "The shipping option is removed from checkout. Orders already placed keep it.", "empty": "No Print Trail shipping options yet.", "created": "Shipping option created", "updated": "Shipping option saved", "deleted": "Shipping option deleted", "name": "Name", "serviceZone": "Service zone", "profile": "Shipping profile", "method": "Print Trail method", "currency": "Currency", "currencyHint": "Three letters, for example GBP", "bands": "Weight bands", "bandsHint": "Each band's limit is in grams and includes the limit. Leave the last band's limit empty for no limit. Prices are as charged, for example 3.95.", "maxWeight": "Up to (g)", "price": "Price", "addBand": "Add band", "removeBand": "Remove", "noLimit": "and over", "band": "{{from}}–{{upTo}} g: {{price}}", "bandOpen": "over {{from}} g: {{price}}", "countries": "Per-country bands", "countriesCount_one": "{{count}} country has its own bands", "countriesCount_other": "{{count}} countries have their own bands", "countriesHint": "A destination listed here is priced from its own bands; any other destination from the weight bands above. Saving keeps them as they are. You can remove a country here, which also clears one the card can't accept. To change a country's bands, or the currency of a card with countries, send the whole card through the Admin API (see the plugin's
|
|
1470
|
+
const shipping = { "title": "Print Trail shipping", "description": "Calculated shipping options priced by weight band. Each option ships with one Print Trail method in one currency.", "back": "Print Trail settings", "create": "Create", "createTitle": "New Print Trail shipping option", "editTitle": "Edit Print Trail shipping option", "deleteTitle": "Delete {{name}}?", "deleteBody": "The shipping option is removed from checkout. Orders already placed keep it.", "empty": "No Print Trail shipping options yet.", "created": "Shipping option created", "updated": "Shipping option saved", "deleted": "Shipping option deleted", "name": "Name", "serviceZone": "Service zone", "profile": "Shipping profile", "method": "Print Trail method", "currency": "Currency", "currencyHint": "Three letters, for example GBP", "bands": "Weight bands", "bandsHint": "Each band's limit is in grams and includes the limit. Leave the last band's limit empty for no limit. Prices are as charged, for example 3.95.", "maxWeight": "Up to (g)", "price": "Price", "addBand": "Add band", "removeBand": "Remove", "noLimit": "and over", "band": "{{from}}–{{upTo}} g: {{price}}", "bandOpen": "over {{from}} g: {{price}}", "countries": "Per-country bands", "countriesCount_one": "{{count}} country has its own bands", "countriesCount_other": "{{count}} countries have their own bands", "countriesHint": "A destination listed here is priced from its own bands; any other destination from the weight bands above. Saving keeps them as they are. You can remove a country here, which also clears one the card can't accept. To change a country's bands, or the currency of a card with countries, send the whole card through the Admin API (see Prices by country in the plugin's reference at docs.printtrail.com/integrations/medusa-reference).", "removeCountry": "Remove", "noCard": "No rate card", "invalidCard": "Invalid rate card", "methodsFallback": "Print Trail couldn't be reached, so its four standard methods are offered.", "select": "Choose…", "readOnly": "Fixed once created", "errors": { "required": "Required", "currency": "Use a three-letter currency code", "noBands": "Add at least one band", "openBandNotLast": "Only the last band can have no limit", "weight": "Use a weight in grams above 0", "ascending": "Each limit must be above the one before", "price": "Use a price of 0 or more", "ambiguousComma": "Write this without a thousands separator: 1250 or 1.25", "countriesCurrency": "Some countries' bands have no price in this currency. Change the currency through the Admin API with every country priced in it, or remove those countries." } };
|
|
1471
1471
|
const order = { "title": "Print Trail", "activity": "Print Trail fulfilments and timeline", "notSent": "Not sent to Print Trail yet", "sendNow": "Send now", "sent": "Sent to Print Trail", "status": "Status", "fulfillable": "Fulfillable", "confirmed": "Confirmed {{at}}", "notConfirmed": "Not confirmed", "blocked": "Confirmation blocked: {{reason}}", "attention": "Needs attention", "pendingRetry": "Retry ({{action}}) due {{at}}", "pendingNow": "{{action}} pending", "cancelStatus": { "label": "Cancel at Print Trail", "requested": "Requested", "awaiting": "Waiting for Print Trail", "cancelled": "Cancelled", "too-late": "Too late", "refused": "Refused", "failed": "Failed", "not-sent": "Nothing was sent" }, "held": "{{count}} items held back", "cancelWarning": "Print Trail has already started on this order. Cancelling it in Medusa may not stop these fulfilments:", "claim": "Claim", "exchange": "Exchange", "reissue": "Re-sent order", "remainder": "Released items", "actions": "Actions", "refresh": "Refresh from Print Trail", "resend": "Resend / retry confirm", "resendAsNew": "Resend as a new Print Trail order", "resendAsNewTitle": "Send as a new Print Trail order?", "resendAsNewBody": "Print Trail cancelled {{number}} and won't reopen it. This creates a new Print Trail order, {{next}}, with the same items.", "dismiss": "Mark as resolved", "dismissTitle": "Mark as resolved?", "dismissBody": `This clears "{{label}}" from the order. It doesn't change anything at Print Trail.`, "dismissBodyPreOrderInactive": `This clears "{{label}}" from the order. It doesn't change anything at Print Trail: the order still waits until the product is made active there. It isn't flagged again while the product stays inactive; if a sync sees it go back on pre-order or live first, it is flagged again when it is next made inactive.`, "dismissBodyPrintTrail": `This clears "{{label}}" from the order. It doesn't change anything at Print Trail. This also resolves the order's other returned, failed or partly dispatched states as they are now, so they aren't flagged next. A parcel that fails or is returned later, or the order coming back to one of these states, is still flagged.`, "dismissed": "Marked as resolved", "updated": "Updated from Print Trail", "noChanges": "No changes at Print Trail", "busy": "Print Trail is busy with this order; try again shortly", "resent": "Sent to Print Trail", "fulfilments": "Fulfilments", "noFulfilments": "Print Trail hasn't split this order into fulfilments yet.", "number": "Number", "location": "Location", "production": "Production", "shippingMethod": "Shipping method", "tracking": "Tracking", "timeline": "Timeline", "noTimeline": "Nothing recorded yet.", "refused": { "already-confirmed": "This order is already confirmed with Print Trail.", "cancelled-in-medusa": "This order is being cancelled at Print Trail.", "print-trail-cancelled": "Print Trail cancelled this order. Use Resend as a new Print Trail order.", "not-cancelled-at-print-trail": "Only an order Print Trail cancelled can be sent again as a new order.", "already-reissued": "This order has already been sent again as a new Print Trail order.", "order-canceled": "The order is cancelled in Medusa.", "changed": "The order changed meanwhile. Refresh the page and try again.", "busy": "Print Trail is busy with this order; try again shortly", "not-dismissable": "This can't be marked as resolved." }, "outcome": { "confirmed": "Sent and confirmed with Print Trail", "not-confirmed": "Sent to Print Trail but not confirmed: {{reason}}", "blocked": "Print Trail didn't confirm it: {{reason}}", "retrying": "Couldn't finish with Print Trail; it will retry. {{reason}}", "held": "Every Print Trail item is held back, so nothing was sent", "not-sent": "Not sent: {{reason}}", "done": "Done" }, "dismissUnchanged": "Nothing to clear: it was already resolved", "fulfilment": { "waiting": "Waiting for pre-order release", "waitingOn": "Waiting for pre-order release (expected {{date}})", "waitingInactive": "Pre-order (product not active)", "outOfStock": "Back-order: out of stock at Print Trail", "warehouse": "Back-order reported by the warehouse" }, "waitingRelease": "Pre-order" };
|
|
1472
1472
|
const variant = { "title": "Print Trail", "sku": "Print Trail SKU", "usesVariantSku": "Uses the variant SKU", "resolved": "Sent as {{sku}}", "noSku": "No SKU", "notInProfile": "This product isn't in a Print Trail shipping profile, so Print Trail doesn't fulfil it.", "stock": { "inStock": "{{available}} in stock", "printOnDemand": "Print on demand ({{available}} in the warehouse), backorders on", "notManaged": "Inventory not managed", "preOrder": "On pre-order at Print Trail; backorders on, so it can be bought at 0 stock", "preOrderOwn": "On pre-order at Print Trail" }, "edit": "Edit SKU", "editTitle": "Print Trail SKU", "override": "SKU override", "hint": "Leave empty to use the variant SKU. Print Trail matches SKUs case-insensitively, so set this only for a SKU that is genuinely different.", "saved": "Print Trail SKU saved", "check": { "matched": "Matched", "inactive": "Inactive at Print Trail", "pre-order": "Pre-order", "not-found": "Not found at Print Trail", "no-sku": "No SKU", "error": "Couldn't check", "checking": "Checking…" }, "numericOverride": "This override was saved as a number, so leading zeros may have been lost. Save it again as text.", "preOrderDate": "Expected {{date}}", "preOrderLimit": "Pre-order limit {{limit}}, {{remaining}} left", "preOrderUnlimited": "No pre-order limit" };
|
|
1473
1473
|
const attention = { "title": "Needs attention", "description": "Print Trail orders the plugin couldn't finish on its own.", "empty": "Nothing needs attention.", "count": "{{count}} Print Trail orders need attention", "view": "View", "order": "Order", "printTrailOrder": "Print Trail order", "issue": "Issue", "lastError": "Last error", "updated": "Updated", "retryCap": "Stopped retrying ({{action}}). Use Resend to try again.", "ptFulfilment": "A fulfilment is {{status}} at Print Trail", "unknown": "Needs attention ({{code}})", "codes": { "pt-cancelled": "Print Trail cancelled this order", "pt-draft-after-confirm": "Back in draft at Print Trail after it was confirmed", "pt-fulfilment-cancelled": "Print Trail cancelled part of this order", "pt-order-missing": "Print Trail can't find this order", "pt-holding": "Holding at Print Trail", "pt-back-order": "On back order at Print Trail", "pt-partial": "Partly dispatched at Print Trail", "pt-returned": "Returned at Print Trail", "pt-fulfilment-returned": "Part of this order was returned at Print Trail", "pt-error": "In error at Print Trail", "pt-invalid": "Invalid at Print Trail", "pt-not-fulfillable": "Print Trail can't fulfil this order", "pt-partially-fulfillable": "Print Trail can fulfil only part of this order", "dispatch-mismatch": "Print Trail's dispatch doesn't match the Medusa order", "medusa-fulfilment-failed": "Medusa refused a fulfilment for a dispatched parcel", "medusa-complete-failed": "Medusa refused to complete the order", "complete-manually": "Everything shipped; complete the order by hand", "dispatched-after-cancel": "Print Trail dispatched after the cancel", "reconcile-failed": "Reconcile keeps failing for this order", "stock-shortfall": "Print Trail is short of stock", "cancel-failed": "The cancel didn't reach Print Trail", "cancel-too-late": "Cancelled too late: Print Trail had already shipped", "amend-refused": "Print Trail refused the order edit", "amend-diverged": "Print Trail's lines no longer match the order", "amend-unsent-items": "Edited items weren't sent to Print Trail", "refunded-not-cancelled": "Fully refunded but not cancelled", "return-before-dispatch": "A return was asked for before dispatch", "no-pt-shipping-method": "A Print Trail item is on an order with no Print Trail shipping", "pt-pre-order-inactive": "A pre-order product isn't active at Print Trail, so the order waits" } };
|
|
@@ -89,7 +89,7 @@ function formatPrice(amount, currency) {
|
|
|
89
89
|
}
|
|
90
90
|
}
|
|
91
91
|
const ENV_SNIPPET = [
|
|
92
|
-
"PRINT_TRAIL_BASE_URL=https://printtrail
|
|
92
|
+
"PRINT_TRAIL_BASE_URL=https://api.printtrail.com",
|
|
93
93
|
"PRINT_TRAIL_CHANNEL_ID=",
|
|
94
94
|
"PRINT_TRAIL_API_TOKEN=",
|
|
95
95
|
"MEDUSA_BACKEND_URL=https://",
|
|
@@ -1464,7 +1464,7 @@ const diagnosis = { "not-configured": "Not connected: the Print Trail API creden
|
|
|
1464
1464
|
const webhook = { "states": { "registered": "Registered here", "not-registered": "Not registered", "elsewhere": "Sending elsewhere", "unknown": "Unknown (couldn't read the channel)", "no-backend-url": "No backend URL (MEDUSA_BACKEND_URL isn't set)" }, "actions": { "registered": "Webhook registered.", "already-registered": "The webhook is already registered here.", "needs-confirmation": "The webhook wasn't changed.", "cannot-register": "The webhook wasn't registered: the channel couldn't be read.", "invalid-url": "The webhook wasn't registered: Print Trail never accepts this address. See the notes under Webhook.", "failed": "Print Trail didn't register the webhook: {{detail}}", "busy": "Another Print Trail connection check is running. Try again in a moment." }, "confirmTitle": "Register this address?", "confirmUnsafe": "Print Trail only sends webhooks to https:// addresses on a public host, and refuses to register any other. {{url}} isn't one, so Print Trail will refuse it unless it's a development Print Trail or the bundled mock. For a local store, use an HTTPS tunnel and set MEDUSA_BACKEND_URL to its address. Register it anyway?", "confirmYes": "Register anyway", "confirmElsewhereTitle": "Send updates to this store?", "confirmElsewhere": "Print Trail is currently sending this channel's updates to {{url}}. Send them to this store instead?", "confirmElsewhereYes": "Send them here" };
|
|
1465
1465
|
const warnings = { "not-configured": "Print Trail isn't connected yet.", "no-webhook-secret": "No webhook secret yet, so every webhook is refused. Press Test connection to fetch it.", "webhook-secret-mismatch": "PRINT_TRAIL_WEBHOOK_SECRET differs from the secret the Print Trail API returned. The plugin uses Print Trail's; remove the variable or update it.", "no-backend-url": "MEDUSA_BACKEND_URL isn't set, so the webhook can't be registered.", "no-admin-url": "MEDUSA_ADMIN_URL isn't set, so Print Trail orders carry no link back to Medusa.", "no-admin-email": "No admin email is set (PRINT_TRAIL_ADMIN_EMAIL), so nobody is emailed when a cancel comes too late.", "in-memory-locking": "Locks are per process. If this store runs more than one server or worker process, configure Medusa's Redis locking provider (@medusajs/medusa/locking-redis), or two processes can send the same order at once.", "postgres-locking": "The Postgres locking provider is the default, and the plugin doesn't support it: it never reclaims an expired lock. Use @medusajs/medusa/locking-redis.", "postgres-locking-configured": "A Postgres locking provider is configured. Don't make it the default: the plugin doesn't support it.", "no-print-trail-shipping-options": "No shipping option uses Print Trail yet, so no order is sent.", "not-https": "Print Trail only sends webhooks to https:// addresses, and refuses to register any other.", "local-address": "This is a local, private or reserved address. Print Trail can't reach it and refuses to register it; for a local store, use an HTTPS tunnel.", "too-long": "This address is longer than 200 characters, which Print Trail never accepts.", "credentials": "This address has a user name or password, which Print Trail never accepts.", "fragment": "This address has a #fragment, which Print Trail never accepts.", "undeliverable": "This address is registered, but Print Trail API 1.2.0 and later deliver nothing to it. Register an https:// address on a public host (an HTTPS tunnel for a local store)." };
|
|
1466
1466
|
const health = { "reconnect": "Reconnect Print Trail", "reconnectBody": "Print Trail refused the connection: the API token or channel ID is wrong. Check PRINT_TRAIL_API_TOKEN and PRINT_TRAIL_CHANNEL_ID, restart Medusa and press Test connection. Orders, cancels and edits waiting meanwhile are retried automatically once it works.", "channelInactive": "The Print Trail channel is inactive", "channelInactiveBody": "Print Trail refuses every request from this channel and sends it no webhooks, so orders aren't sent. Make the channel active in Print Trail (Settings → Channels), then press Test connection. Orders, cancels and edits waiting meanwhile are retried automatically once it is active.", "unmatched": "{{count}} SKUs aren't known to Print Trail.", "unmatchedLink": "See SKU health" };
|
|
1467
|
-
const shipping = { "title": "Print Trail shipping", "description": "Calculated shipping options priced by weight band. Each option ships with one Print Trail method in one currency.", "back": "Print Trail settings", "create": "Create", "createTitle": "New Print Trail shipping option", "editTitle": "Edit Print Trail shipping option", "deleteTitle": "Delete {{name}}?", "deleteBody": "The shipping option is removed from checkout. Orders already placed keep it.", "empty": "No Print Trail shipping options yet.", "created": "Shipping option created", "updated": "Shipping option saved", "deleted": "Shipping option deleted", "name": "Name", "serviceZone": "Service zone", "profile": "Shipping profile", "method": "Print Trail method", "currency": "Currency", "currencyHint": "Three letters, for example GBP", "bands": "Weight bands", "bandsHint": "Each band's limit is in grams and includes the limit. Leave the last band's limit empty for no limit. Prices are as charged, for example 3.95.", "maxWeight": "Up to (g)", "price": "Price", "addBand": "Add band", "removeBand": "Remove", "noLimit": "and over", "band": "{{from}}–{{upTo}} g: {{price}}", "bandOpen": "over {{from}} g: {{price}}", "countries": "Per-country bands", "countriesCount_one": "{{count}} country has its own bands", "countriesCount_other": "{{count}} countries have their own bands", "countriesHint": "A destination listed here is priced from its own bands; any other destination from the weight bands above. Saving keeps them as they are. You can remove a country here, which also clears one the card can't accept. To change a country's bands, or the currency of a card with countries, send the whole card through the Admin API (see the plugin's
|
|
1467
|
+
const shipping = { "title": "Print Trail shipping", "description": "Calculated shipping options priced by weight band. Each option ships with one Print Trail method in one currency.", "back": "Print Trail settings", "create": "Create", "createTitle": "New Print Trail shipping option", "editTitle": "Edit Print Trail shipping option", "deleteTitle": "Delete {{name}}?", "deleteBody": "The shipping option is removed from checkout. Orders already placed keep it.", "empty": "No Print Trail shipping options yet.", "created": "Shipping option created", "updated": "Shipping option saved", "deleted": "Shipping option deleted", "name": "Name", "serviceZone": "Service zone", "profile": "Shipping profile", "method": "Print Trail method", "currency": "Currency", "currencyHint": "Three letters, for example GBP", "bands": "Weight bands", "bandsHint": "Each band's limit is in grams and includes the limit. Leave the last band's limit empty for no limit. Prices are as charged, for example 3.95.", "maxWeight": "Up to (g)", "price": "Price", "addBand": "Add band", "removeBand": "Remove", "noLimit": "and over", "band": "{{from}}–{{upTo}} g: {{price}}", "bandOpen": "over {{from}} g: {{price}}", "countries": "Per-country bands", "countriesCount_one": "{{count}} country has its own bands", "countriesCount_other": "{{count}} countries have their own bands", "countriesHint": "A destination listed here is priced from its own bands; any other destination from the weight bands above. Saving keeps them as they are. You can remove a country here, which also clears one the card can't accept. To change a country's bands, or the currency of a card with countries, send the whole card through the Admin API (see Prices by country in the plugin's reference at docs.printtrail.com/integrations/medusa-reference).", "removeCountry": "Remove", "noCard": "No rate card", "invalidCard": "Invalid rate card", "methodsFallback": "Print Trail couldn't be reached, so its four standard methods are offered.", "select": "Choose…", "readOnly": "Fixed once created", "errors": { "required": "Required", "currency": "Use a three-letter currency code", "noBands": "Add at least one band", "openBandNotLast": "Only the last band can have no limit", "weight": "Use a weight in grams above 0", "ascending": "Each limit must be above the one before", "price": "Use a price of 0 or more", "ambiguousComma": "Write this without a thousands separator: 1250 or 1.25", "countriesCurrency": "Some countries' bands have no price in this currency. Change the currency through the Admin API with every country priced in it, or remove those countries." } };
|
|
1468
1468
|
const order = { "title": "Print Trail", "activity": "Print Trail fulfilments and timeline", "notSent": "Not sent to Print Trail yet", "sendNow": "Send now", "sent": "Sent to Print Trail", "status": "Status", "fulfillable": "Fulfillable", "confirmed": "Confirmed {{at}}", "notConfirmed": "Not confirmed", "blocked": "Confirmation blocked: {{reason}}", "attention": "Needs attention", "pendingRetry": "Retry ({{action}}) due {{at}}", "pendingNow": "{{action}} pending", "cancelStatus": { "label": "Cancel at Print Trail", "requested": "Requested", "awaiting": "Waiting for Print Trail", "cancelled": "Cancelled", "too-late": "Too late", "refused": "Refused", "failed": "Failed", "not-sent": "Nothing was sent" }, "held": "{{count}} items held back", "cancelWarning": "Print Trail has already started on this order. Cancelling it in Medusa may not stop these fulfilments:", "claim": "Claim", "exchange": "Exchange", "reissue": "Re-sent order", "remainder": "Released items", "actions": "Actions", "refresh": "Refresh from Print Trail", "resend": "Resend / retry confirm", "resendAsNew": "Resend as a new Print Trail order", "resendAsNewTitle": "Send as a new Print Trail order?", "resendAsNewBody": "Print Trail cancelled {{number}} and won't reopen it. This creates a new Print Trail order, {{next}}, with the same items.", "dismiss": "Mark as resolved", "dismissTitle": "Mark as resolved?", "dismissBody": `This clears "{{label}}" from the order. It doesn't change anything at Print Trail.`, "dismissBodyPreOrderInactive": `This clears "{{label}}" from the order. It doesn't change anything at Print Trail: the order still waits until the product is made active there. It isn't flagged again while the product stays inactive; if a sync sees it go back on pre-order or live first, it is flagged again when it is next made inactive.`, "dismissBodyPrintTrail": `This clears "{{label}}" from the order. It doesn't change anything at Print Trail. This also resolves the order's other returned, failed or partly dispatched states as they are now, so they aren't flagged next. A parcel that fails or is returned later, or the order coming back to one of these states, is still flagged.`, "dismissed": "Marked as resolved", "updated": "Updated from Print Trail", "noChanges": "No changes at Print Trail", "busy": "Print Trail is busy with this order; try again shortly", "resent": "Sent to Print Trail", "fulfilments": "Fulfilments", "noFulfilments": "Print Trail hasn't split this order into fulfilments yet.", "number": "Number", "location": "Location", "production": "Production", "shippingMethod": "Shipping method", "tracking": "Tracking", "timeline": "Timeline", "noTimeline": "Nothing recorded yet.", "refused": { "already-confirmed": "This order is already confirmed with Print Trail.", "cancelled-in-medusa": "This order is being cancelled at Print Trail.", "print-trail-cancelled": "Print Trail cancelled this order. Use Resend as a new Print Trail order.", "not-cancelled-at-print-trail": "Only an order Print Trail cancelled can be sent again as a new order.", "already-reissued": "This order has already been sent again as a new Print Trail order.", "order-canceled": "The order is cancelled in Medusa.", "changed": "The order changed meanwhile. Refresh the page and try again.", "busy": "Print Trail is busy with this order; try again shortly", "not-dismissable": "This can't be marked as resolved." }, "outcome": { "confirmed": "Sent and confirmed with Print Trail", "not-confirmed": "Sent to Print Trail but not confirmed: {{reason}}", "blocked": "Print Trail didn't confirm it: {{reason}}", "retrying": "Couldn't finish with Print Trail; it will retry. {{reason}}", "held": "Every Print Trail item is held back, so nothing was sent", "not-sent": "Not sent: {{reason}}", "done": "Done" }, "dismissUnchanged": "Nothing to clear: it was already resolved", "fulfilment": { "waiting": "Waiting for pre-order release", "waitingOn": "Waiting for pre-order release (expected {{date}})", "waitingInactive": "Pre-order (product not active)", "outOfStock": "Back-order: out of stock at Print Trail", "warehouse": "Back-order reported by the warehouse" }, "waitingRelease": "Pre-order" };
|
|
1469
1469
|
const variant = { "title": "Print Trail", "sku": "Print Trail SKU", "usesVariantSku": "Uses the variant SKU", "resolved": "Sent as {{sku}}", "noSku": "No SKU", "notInProfile": "This product isn't in a Print Trail shipping profile, so Print Trail doesn't fulfil it.", "stock": { "inStock": "{{available}} in stock", "printOnDemand": "Print on demand ({{available}} in the warehouse), backorders on", "notManaged": "Inventory not managed", "preOrder": "On pre-order at Print Trail; backorders on, so it can be bought at 0 stock", "preOrderOwn": "On pre-order at Print Trail" }, "edit": "Edit SKU", "editTitle": "Print Trail SKU", "override": "SKU override", "hint": "Leave empty to use the variant SKU. Print Trail matches SKUs case-insensitively, so set this only for a SKU that is genuinely different.", "saved": "Print Trail SKU saved", "check": { "matched": "Matched", "inactive": "Inactive at Print Trail", "pre-order": "Pre-order", "not-found": "Not found at Print Trail", "no-sku": "No SKU", "error": "Couldn't check", "checking": "Checking…" }, "numericOverride": "This override was saved as a number, so leading zeros may have been lost. Save it again as text.", "preOrderDate": "Expected {{date}}", "preOrderLimit": "Pre-order limit {{limit}}, {{remaining}} left", "preOrderUnlimited": "No pre-order limit" };
|
|
1470
1470
|
const attention = { "title": "Needs attention", "description": "Print Trail orders the plugin couldn't finish on its own.", "empty": "Nothing needs attention.", "count": "{{count}} Print Trail orders need attention", "view": "View", "order": "Order", "printTrailOrder": "Print Trail order", "issue": "Issue", "lastError": "Last error", "updated": "Updated", "retryCap": "Stopped retrying ({{action}}). Use Resend to try again.", "ptFulfilment": "A fulfilment is {{status}} at Print Trail", "unknown": "Needs attention ({{code}})", "codes": { "pt-cancelled": "Print Trail cancelled this order", "pt-draft-after-confirm": "Back in draft at Print Trail after it was confirmed", "pt-fulfilment-cancelled": "Print Trail cancelled part of this order", "pt-order-missing": "Print Trail can't find this order", "pt-holding": "Holding at Print Trail", "pt-back-order": "On back order at Print Trail", "pt-partial": "Partly dispatched at Print Trail", "pt-returned": "Returned at Print Trail", "pt-fulfilment-returned": "Part of this order was returned at Print Trail", "pt-error": "In error at Print Trail", "pt-invalid": "Invalid at Print Trail", "pt-not-fulfillable": "Print Trail can't fulfil this order", "pt-partially-fulfillable": "Print Trail can fulfil only part of this order", "dispatch-mismatch": "Print Trail's dispatch doesn't match the Medusa order", "medusa-fulfilment-failed": "Medusa refused a fulfilment for a dispatched parcel", "medusa-complete-failed": "Medusa refused to complete the order", "complete-manually": "Everything shipped; complete the order by hand", "dispatched-after-cancel": "Print Trail dispatched after the cancel", "reconcile-failed": "Reconcile keeps failing for this order", "stock-shortfall": "Print Trail is short of stock", "cancel-failed": "The cancel didn't reach Print Trail", "cancel-too-late": "Cancelled too late: Print Trail had already shipped", "amend-refused": "Print Trail refused the order edit", "amend-diverged": "Print Trail's lines no longer match the order", "amend-unsent-items": "Edited items weren't sent to Print Trail", "refunded-not-cancelled": "Fully refunded but not cancelled", "return-before-dispatch": "A return was asked for before dispatch", "no-pt-shipping-method": "A Print Trail item is on an order with no Print Trail shipping", "pt-pre-order-inactive": "A pre-order product isn't active at Print Trail, so the order waits" } };
|
|
@@ -32,8 +32,8 @@ function resolvePluginOptions(raw) {
|
|
|
32
32
|
const channelId = clean(o.channelId);
|
|
33
33
|
const apiToken = clean(o.apiToken);
|
|
34
34
|
if ((channelId || apiToken) && !baseUrl) {
|
|
35
|
-
throw new utils_1.MedusaError(utils_1.MedusaError.Types.INVALID_DATA,
|
|
36
|
-
|
|
35
|
+
throw new utils_1.MedusaError(utils_1.MedusaError.Types.INVALID_DATA, "Print Trail API: PRINT_TRAIL_BASE_URL is not set. Use https://api.printtrail.com, " +
|
|
36
|
+
`or ${exports.STAGING_HOST} for staging.`);
|
|
37
37
|
}
|
|
38
38
|
return {
|
|
39
39
|
baseUrl,
|
|
@@ -62,4 +62,4 @@ function resolvePluginOptions(raw) {
|
|
|
62
62
|
stockLocationIds: o.stockLocationIds,
|
|
63
63
|
};
|
|
64
64
|
}
|
|
65
|
-
//# sourceMappingURL=data:application/json;base64,
|
|
65
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY29uZmlnLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vLi4vc3JjL2NvbmZpZy50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOzs7QUFtREEsNERBUUM7QUFFRCxrQ0FFQztBQXNCRCxvREE0Q0M7QUFqSUQscURBQXVEO0FBRTFDLFFBQUEsWUFBWSxHQUFHLDJDQUEyQyxDQUFBO0FBd0N2RSxNQUFNLEtBQUssR0FBRyxDQUFDLEtBQXlCLEVBQUUsRUFBRTtJQUMxQyxNQUFNLENBQUMsR0FBRyxLQUFLLEVBQUUsSUFBSSxFQUFFLENBQUE7SUFDdkIsT0FBTyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsU0FBUyxDQUFBO0FBQzFCLENBQUMsQ0FBQTtBQUVELE1BQU0sVUFBVSxHQUFHLENBQUMsR0FBdUIsRUFBRSxFQUFFLENBQUMsR0FBRyxFQUFFLE9BQU8sQ0FBQyxNQUFNLEVBQUUsRUFBRSxDQUFDLENBQUE7QUFJeEUsU0FBZ0Isd0JBQXdCLENBQ3RDLE1BQXlCLE9BQU8sQ0FBQyxHQUFHO0lBRXBDLE9BQU87UUFDTCxPQUFPLEVBQUUsVUFBVSxDQUFDLEtBQUssQ0FBQyxHQUFHLENBQUMsb0JBQW9CLENBQUMsQ0FBQztRQUNwRCxTQUFTLEVBQUUsS0FBSyxDQUFDLEdBQUcsQ0FBQyxzQkFBc0IsQ0FBQztRQUM1QyxRQUFRLEVBQUUsS0FBSyxDQUFDLEdBQUcsQ0FBQyxxQkFBcUIsQ0FBQztLQUMzQyxDQUFBO0FBQ0gsQ0FBQztBQUVELFNBQWdCLFdBQVcsQ0FBQyxDQUF3QjtJQUNsRCxPQUFPLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxPQUFPLElBQUksQ0FBQyxDQUFDLFNBQVMsSUFBSSxDQUFDLENBQUMsUUFBUSxDQUFDLENBQUE7QUFDbkQsQ0FBQztBQWNELE1BQU0sVUFBVSxHQUFHLENBQUMsS0FBbUMsRUFBRSxRQUFnQixFQUFrQixFQUFFLENBQzNGLEtBQUssS0FBSyxLQUFLLENBQUMsQ0FBQyxDQUFDLEtBQUssQ0FBQyxDQUFDLENBQUMsT0FBTyxLQUFLLEtBQUssUUFBUSxJQUFJLEtBQUssQ0FBQyxJQUFJLEVBQUUsQ0FBQyxDQUFDLENBQUMsS0FBSyxDQUFDLENBQUMsQ0FBQyxRQUFRLENBQUE7QUFFeEYsTUFBTSxTQUFTLEdBQUcsQ0FBQyxLQUFjLEVBQVksRUFBRTtJQUM3QyxNQUFNLEtBQUssR0FBRyxLQUFLLENBQUMsT0FBTyxDQUFDLEtBQUssQ0FBQyxDQUFDLENBQUMsQ0FBQyxLQUFLLENBQUMsQ0FBQyxDQUFDLE9BQU8sS0FBSyxLQUFLLFFBQVEsQ0FBQyxDQUFDLENBQUMsS0FBSyxDQUFDLEtBQUssQ0FBQyxHQUFHLENBQUMsQ0FBQyxDQUFDLENBQUMsRUFBRSxDQUFBO0lBQzlGLE9BQU8sS0FBSyxDQUFDLE9BQU8sQ0FBQyxDQUFDLENBQUMsRUFBRSxFQUFFLENBQUMsQ0FBQyxPQUFPLENBQUMsS0FBSyxRQUFRLElBQUksQ0FBQyxDQUFDLElBQUksRUFBRSxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxJQUFJLEVBQUUsQ0FBQyxDQUFDLENBQUMsQ0FBQyxFQUFFLENBQUMsQ0FBQyxDQUFBO0FBQ3BGLENBQUMsQ0FBQTtBQUVELFNBQWdCLG9CQUFvQixDQUNsQyxHQUE4RTtJQUU5RSxNQUFNLENBQUMsR0FBRyxDQUFDLEdBQUcsSUFBSSxFQUFFLENBQStCLENBQUE7SUFDbkQsTUFBTSxPQUFPLEdBQUcsVUFBVSxDQUFDLEtBQUssQ0FBQyxDQUFDLENBQUMsT0FBTyxDQUFDLENBQUMsQ0FBQTtJQUM1QyxNQUFNLFNBQVMsR0FBRyxLQUFLLENBQUMsQ0FBQyxDQUFDLFNBQVMsQ0FBQyxDQUFBO0lBQ3BDLE1BQU0sUUFBUSxHQUFHLEtBQUssQ0FBQyxDQUFDLENBQUMsUUFBUSxDQUFDLENBQUE7SUFJbEMsSUFBSSxDQUFDLFNBQVMsSUFBSSxRQUFRLENBQUMsSUFBSSxDQUFDLE9BQU8sRUFBRSxDQUFDO1FBQ3hDLE1BQU0sSUFBSSxtQkFBVyxDQUNuQixtQkFBVyxDQUFDLEtBQUssQ0FBQyxZQUFZLEVBQzlCLG9GQUFvRjtZQUNsRixNQUFNLG9CQUFZLGVBQWUsQ0FDcEMsQ0FBQTtJQUNILENBQUM7SUFFRCxPQUFPO1FBQ0wsT0FBTztRQUNQLFNBQVM7UUFDVCxRQUFRO1FBQ1IsYUFBYSxFQUFFLEtBQUssQ0FBQyxDQUFDLENBQUMsYUFBYSxDQUFDO1FBQ3JDLFVBQVUsRUFBRSxVQUFVLENBQUMsS0FBSyxDQUFDLENBQUMsQ0FBQyxVQUFVLENBQUMsQ0FBQztRQUMzQyxRQUFRLEVBQUUsVUFBVSxDQUFDLEtBQUssQ0FBQyxDQUFDLENBQUMsUUFBUSxDQUFDLENBQUM7UUFDdkMsTUFBTSxFQUFFLFVBQVUsQ0FBQyxLQUFLLENBQUMsQ0FBQyxDQUFDLE1BQU0sQ0FBQyxDQUFDO1FBQ25DLGlCQUFpQixFQUFFLENBQUMsQ0FBQyxpQkFBaUIsSUFBSSxFQUFFO1FBQzVDLHFCQUFxQixFQUFFLENBQUMsQ0FBQyxxQkFBcUIsSUFBSSxVQUFVO1FBQzVELFNBQVMsRUFBRSxDQUFDLENBQUMsU0FBUyxJQUFJLGNBQWM7UUFDeEMsVUFBVSxFQUFFLENBQUMsQ0FBQyxVQUFVLElBQUksSUFBSTtRQUNoQyxTQUFTLEVBQUUsRUFBRSxPQUFPLEVBQUUsQ0FBQyxDQUFDLFNBQVMsRUFBRSxPQUFPLElBQUksSUFBSSxFQUFFO1FBQ3BELFNBQVMsRUFBRTtZQUNULE9BQU8sRUFBRSxDQUFDLENBQUMsU0FBUyxFQUFFLE9BQU8sSUFBSSxJQUFJO1lBQ3JDLFlBQVksRUFBRSxDQUFDLENBQUMsU0FBUyxFQUFFLFlBQVksSUFBSSxDQUFDO1NBQzdDO1FBQ0QsWUFBWSxFQUFFLENBQUMsQ0FBQyxZQUFZLElBQUksSUFBSTtRQUNwQyxhQUFhLEVBQUU7WUFDYixRQUFRLEVBQUUsVUFBVSxDQUFDLENBQUMsQ0FBQyxhQUFhLEVBQUUsUUFBUSxFQUFFLHNCQUFzQixDQUFDO1lBQ3ZFLGFBQWEsRUFBRSxVQUFVLENBQUMsQ0FBQyxDQUFDLGFBQWEsRUFBRSxhQUFhLEVBQUUsNkJBQTZCLENBQUM7WUFDeEYsVUFBVSxFQUFFLFNBQVMsQ0FBQyxDQUFDLENBQUMsYUFBYSxFQUFFLFVBQVUsQ0FBQztZQUNsRCxNQUFNLEVBQUUsQ0FBQyxDQUFDLGFBQWEsRUFBRSxNQUFNLElBQUksSUFBSTtTQUN4QztRQUNELGdCQUFnQixFQUFFLENBQUMsQ0FBQyxnQkFBZ0I7S0FDckMsQ0FBQTtBQUNILENBQUMifQ==
|
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,15 @@ All notable changes to `medusa-plugin-print-trail` are recorded here.
|
|
|
4
4
|
The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the
|
|
5
5
|
package uses [semantic versioning](https://semver.org/).
|
|
6
6
|
|
|
7
|
+
## [1.0.1] - 2026-10-27
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
- **The README is short.** It says what the plugin does, what it needs and how to set it up. The rest is on docs.printtrail.com: the [guide](https://docs.printtrail.com/integrations/medusa/) for setting up and day-to-day use, and the [reference](https://docs.printtrail.com/integrations/medusa-reference/) for the options, rate cards, pre-orders, notifications, the developer surface and troubleshooting. The README no longer lists Print Trail API versions.
|
|
12
|
+
- The "Package entry points" list that 1.0.0's upgrade note refers to is now in the [reference](https://docs.printtrail.com/integrations/medusa-reference/).
|
|
13
|
+
- The hint under a shipping option's per-country bands points at the reference instead of the README.
|
|
14
|
+
- The production Print Trail API host, `https://api.printtrail.com`, is named where the plugin asks for `PRINT_TRAIL_BASE_URL`. The error for a missing base URL no longer says the host is shown in the Print Trail app, and the settings page's setup snippet uses the production host instead of staging.
|
|
15
|
+
|
|
7
16
|
## [1.0.0] - 2026-10-26
|
|
8
17
|
|
|
9
18
|
**Upgrading from 0.2.0 (and 0.1.x).** From 0.1.x, first follow 0.2.0's upgrade note below (the package was renamed). Then:
|
package/README.md
CHANGED
|
@@ -1,47 +1,34 @@
|
|
|
1
1
|
# Print Trail for Medusa
|
|
2
2
|
|
|
3
|
-
`medusa-plugin-print-trail` is a Medusa v2 plugin that connects a Medusa store to the [Print Trail API](https://docs.printtrail.com/developers/api-documentation/).
|
|
3
|
+
`medusa-plugin-print-trail` is a Medusa v2 plugin that connects a Medusa store to the [Print Trail API](https://docs.printtrail.com/developers/api-documentation/) for automated order fulfilment of books and printed products.
|
|
4
4
|
|
|
5
5
|
- Orders containing Print Trail products are sent to Print Trail and confirmed automatically.
|
|
6
|
-
-
|
|
7
|
-
- Stock levels are synced from Print Trail into Medusa inventory.
|
|
8
|
-
-
|
|
6
|
+
- Each Print Trail parcel comes back as a Medusa fulfilment and shipment, with its tracking number and its own shipment email.
|
|
7
|
+
- Stock levels, pre-orders included, are synced from Print Trail into Medusa inventory.
|
|
8
|
+
- Cancels, order edits, claims and exchanges made in Medusa are carried to Print Trail.
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
Website: [printtrail.com](https://printtrail.com/). Guide: [docs.printtrail.com/integrations/medusa](https://docs.printtrail.com/integrations/medusa/).
|
|
11
11
|
|
|
12
12
|
## Screenshots
|
|
13
13
|
|
|
14
14
|

|
|
15
15
|
|
|
16
|
-
The order page: each Print Trail fulfilment with its
|
|
16
|
+
The order page: each Print Trail fulfilment with its tracking, and the order's Print Trail timeline.
|
|
17
17
|
|
|
18
|
-

|
|
19
19
|
|
|
20
|
-
Settings → Print Trail: the connection
|
|
20
|
+
Settings → Print Trail: the connection and the webhook.
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
## Requirements
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
- Medusa `^2.17.2`, tested on 2.17.2 and 2.22.1 with Node 22.
|
|
25
|
+
- A Print Trail account with a channel of its own for the store (not the Standard channel), and that channel's API token. Both are in the Print Trail app under Settings → Channels: **Token** on the channel's row creates the token, which is shown once.
|
|
26
|
+
- Redis locking (Medusa's Locking Module with its Redis provider) when the store runs more than one process (a server and a worker count as two). Without a shared lock, two processes can send the same order twice.
|
|
27
|
+
- An email provider in Medusa (a notification provider on the `email` channel) for the shipment and admin emails. Without one, none is sent.
|
|
25
28
|
|
|
26
|
-
##
|
|
29
|
+
## Setup
|
|
27
30
|
|
|
28
|
-
|
|
29
|
-
- `engines` is `node >=20`, Medusa 2.17's own floor. Node 20 is untested. Medusa itself needs more from 2.19 (`^20.19 || >=22.12`) and from 2.22 (`>=22.22.0`).
|
|
30
|
-
|
|
31
|
-
### Print Trail API versions
|
|
32
|
-
|
|
33
|
-
Print Trail sends its API version with every response. The settings page shows the last one the store heard ("Print Trail API version"). Each feature below is used whenever Print Trail sends its field, whatever the version says; the version is only shown.
|
|
34
|
-
|
|
35
|
-
| Print Trail API | Features |
|
|
36
|
-
|---|---|
|
|
37
|
-
| 1.0.0 | Everything else in this README. |
|
|
38
|
-
| 1.1.0 | The webhook secret returned by webhook registration, Rotate secret, `confirmedAt` on orders, and typed duplicate-create 409s (fewer lookups by order number). |
|
|
39
|
-
| 1.2.0 | Production since 2026-10-20: webhook URLs must be `https` on a public host, an unknown channel ID is a 401, inactive channels and teams are refused, `Retry-After` on 429s, a `reference` on 500s, and input limits. Nothing to enable; see Troubleshooting. |
|
|
40
|
-
| 1.3.0 | Pre-order products (below; production since 2026-10-20): pre-order lines held and released by Print Trail, `backOrderReason` and `sentToWarehouseAt` on fulfilments, a confirm that becomes a back-order instead of a stock refusal, and pre-order status, dates and limits on stock. |
|
|
41
|
-
| 1.4.0 | Test-mode channels: orders and webhooks carry a `mode` of `live` or `test`, and a test channel's orders are checked and stored but never reach the warehouse, so they stay processing with no dispatch. The plugin doesn't read `mode`, so nothing changes. |
|
|
42
|
-
| 1.5.0 | GTINs at a consistent length (a book's is its ISBN-13, a UPC-A's its 13-digit form), with `ean` and `upc` filled in more often. The plugin doesn't read or send GTINs, so nothing changes. |
|
|
43
|
-
|
|
44
|
-
## Install
|
|
31
|
+
### 1. Install
|
|
45
32
|
|
|
46
33
|
In the store:
|
|
47
34
|
|
|
@@ -49,20 +36,12 @@ In the store:
|
|
|
49
36
|
npm install medusa-plugin-print-trail
|
|
50
37
|
```
|
|
51
38
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
```bash
|
|
55
|
-
npx medusa db:migrate
|
|
56
|
-
```
|
|
57
|
-
|
|
58
|
-
## Configure
|
|
59
|
-
|
|
60
|
-
### `medusa-config.ts`
|
|
39
|
+
### 2. Configure `medusa-config.ts`
|
|
61
40
|
|
|
62
41
|
```ts
|
|
63
42
|
import { printTrailOptionsFromEnv } from "medusa-plugin-print-trail/config"
|
|
64
43
|
|
|
65
|
-
const printTrail = printTrailOptionsFromEnv()
|
|
44
|
+
const printTrail = printTrailOptionsFromEnv()
|
|
66
45
|
|
|
67
46
|
module.exports = defineConfig({
|
|
68
47
|
// …
|
|
@@ -70,22 +49,9 @@ module.exports = defineConfig({
|
|
|
70
49
|
resolve: "medusa-plugin-print-trail",
|
|
71
50
|
options: {
|
|
72
51
|
...printTrail,
|
|
73
|
-
backendUrl: process.env.MEDUSA_BACKEND_URL,
|
|
74
|
-
adminUrl: process.env.MEDUSA_ADMIN_URL,
|
|
75
|
-
|
|
76
|
-
orderNumberPrefix: "SHOP-", // Print Trail orderNumber = prefix + display_id [+ "-n"]
|
|
77
|
-
defaultShippingMethod: "standard",
|
|
78
|
-
confirmOn: "order.placed", // or "payment.captured" for manual/BACS-style payments
|
|
79
|
-
strictSkus: true,
|
|
80
|
-
stockSync: { enabled: true },
|
|
81
|
-
reconcile: { enabled: true, lookbackDays: 7 },
|
|
82
|
-
autoComplete: true,
|
|
83
|
-
notifications: {
|
|
84
|
-
shipment: "print-trail-shipment", // template id, or false
|
|
85
|
-
cancelTooLate: "print-trail-cancel-too-late", // template id, or false
|
|
86
|
-
adminEmail: process.env.PRINT_TRAIL_ADMIN_EMAIL, // shop admin address(es) for alerts
|
|
87
|
-
},
|
|
88
|
-
stockLocationIds: undefined, // default: every location stocking the SKU
|
|
52
|
+
backendUrl: process.env.MEDUSA_BACKEND_URL,
|
|
53
|
+
adminUrl: process.env.MEDUSA_ADMIN_URL,
|
|
54
|
+
notifications: { adminEmail: process.env.PRINT_TRAIL_ADMIN_EMAIL },
|
|
89
55
|
},
|
|
90
56
|
}],
|
|
91
57
|
modules: [{
|
|
@@ -104,449 +70,47 @@ module.exports = defineConfig({
|
|
|
104
70
|
})
|
|
105
71
|
```
|
|
106
72
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
- the `plugins` entry configures everything else: the plugin's module, workflows, API routes, jobs and subscribers;
|
|
110
|
-
- the fulfilment provider entry needs only the three credentials. It lists Print Trail's shipping methods and prices options from their rate cards. It is registered with `id: "print-trail"`, so Print Trail shipping options carry `provider_id` `print-trail_print-trail`.
|
|
111
|
-
|
|
112
|
-
`printTrailOptionsFromEnv()` reads the three credentials once, so both blocks get the same values.
|
|
113
|
-
|
|
114
|
-
**Keep the manual provider listed.** Setting `providers` replaces Medusa's default list. Without the `fulfillment-manual` entry, the store's existing manual shipping options stop working. If the store already has an `@medusajs/medusa/fulfillment` entry, add the Print Trail provider to it rather than adding a second entry.
|
|
115
|
-
|
|
116
|
-
### Environment variables
|
|
117
|
-
|
|
118
|
-
The plugin itself reads only `PRINT_TRAIL_BASE_URL`, `PRINT_TRAIL_CHANNEL_ID` and `PRINT_TRAIL_API_TOKEN` (through `printTrailOptionsFromEnv()`), plus the three schedule variables below. The other names are the ones the sample config passes through, so a store may use different names.
|
|
119
|
-
|
|
120
|
-
| Variable | Option | Meaning |
|
|
121
|
-
|---|---|---|
|
|
122
|
-
| `PRINT_TRAIL_BASE_URL` | `baseUrl` | The Print Trail API host. No default (see below). |
|
|
123
|
-
| `PRINT_TRAIL_CHANNEL_ID` | `channelId` | The Print Trail channel for this store. |
|
|
124
|
-
| `PRINT_TRAIL_API_TOKEN` | `apiToken` | The channel's API token. |
|
|
125
|
-
| `PRINT_TRAIL_WEBHOOK_SECRET` | `webhookSecret` | Optional. Only for a Print Trail API that doesn't return the webhook secret when the webhook is registered (see "The webhook secret" below). Pass it as `webhookSecret: process.env.PRINT_TRAIL_WEBHOOK_SECRET`. |
|
|
126
|
-
| `MEDUSA_BACKEND_URL` | `backendUrl` | This backend's public URL. The webhook is registered at `{MEDUSA_BACKEND_URL}/hooks/print-trail`. |
|
|
127
|
-
| `MEDUSA_ADMIN_URL` | `adminUrl` | The Medusa admin URL. Print Trail orders link back to `{adminUrl}/orders/{id}`, and so does the cancel-too-late email. |
|
|
128
|
-
| `PRINT_TRAIL_ADMIN_EMAIL` | `notifications.adminEmail` | Who gets the cancel-too-late email. A comma-separated list is accepted. |
|
|
129
|
-
|
|
130
|
-
**`baseUrl` has no default**, so nothing reaches Print Trail unless someone chose a host. Use `https://printtrail-core.consilstaging.com` for staging, or the production host shown in the Print Trail app.
|
|
131
|
-
|
|
132
|
-
- With none of the three credentials set, the plugin loads as "not connected" and sends nothing.
|
|
133
|
-
- With a channel id or token set but no base URL, the plugin's module fails to load at startup, with a message naming the staging host.
|
|
134
|
-
|
|
135
|
-
Changing any credential needs a restart.
|
|
136
|
-
|
|
137
|
-
### Job schedules
|
|
138
|
-
|
|
139
|
-
A scheduled job's schedule is fixed when its file loads, before plugin options are available. So the schedules come from environment variables, not options:
|
|
140
|
-
|
|
141
|
-
| Variable | Job | Default |
|
|
142
|
-
|---|---|---|
|
|
143
|
-
| `PRINT_TRAIL_SWEEP_SCHEDULE` | `print-trail-sweep`: due retries, orders whose `order.placed` was missed, re-fetches, deferred stock syncs and SKU checks | `*/5 * * * *` |
|
|
144
|
-
| `PRINT_TRAIL_RECONCILE_SCHEDULE` | `print-trail-reconcile`: the `updated-since` poll that catches what webhooks missed | `0 */6 * * *` |
|
|
145
|
-
| `PRINT_TRAIL_STOCK_SCHEDULE` | `print-trail-stock-sync`: the full stock sync | `0 3 * * *` |
|
|
146
|
-
|
|
147
|
-
Set them in both the server's and the worker's environment. The settings page shows the server process's values.
|
|
148
|
-
|
|
149
|
-
### Options
|
|
150
|
-
|
|
151
|
-
Every option of the `plugins` entry, as `resolvePluginOptions` reads it:
|
|
152
|
-
|
|
153
|
-
| Option | Default | Meaning |
|
|
154
|
-
|---|---|---|
|
|
155
|
-
| `baseUrl` | none | Print Trail API host. Required once a channel id or token is set. |
|
|
156
|
-
| `channelId` | none | Print Trail channel id. |
|
|
157
|
-
| `apiToken` | none | Print Trail API token. Never logged and never returned by the admin API. |
|
|
158
|
-
| `webhookSecret` | none | Fallback secret for the HMAC-SHA256 `Signature` header on webhooks, used only until Print Trail has returned one (below). With neither, every webhook is refused (401). |
|
|
159
|
-
| `backendUrl` | none | Public URL of this backend, for webhook registration. Without it, Test connection can't register the webhook. |
|
|
160
|
-
| `adminUrl` | none | Medusa admin URL, for the link sent to Print Trail as `orderUrl` and the admin email's order link. |
|
|
161
|
-
| `appUrl` | derived | Print Trail app URL, for links into Print Trail. By default it comes from `baseUrl`: an `api.` host becomes `app.`, and any other host is used as it is. |
|
|
162
|
-
| `orderNumberPrefix` | `""` | Print Trail order number is prefix + Medusa `display_id`, with `-n` for the order's second and later Print Trail orders (claims, exchanges, released lines). |
|
|
163
|
-
| `defaultShippingMethod` | `"standard"` | Print Trail shipping method for an option whose `data` has no `method`. It is also the confirm request's default method. |
|
|
164
|
-
| `confirmOn` | `"order.placed"` | When the Print Trail order is confirmed. `"payment.captured"` waits for a capture (manual or bank-transfer payments). The draft is sent at placement either way. |
|
|
165
|
-
| `strictSkus` | `true` | Ask Print Trail to refuse the order if any SKU is unknown. The order is then blocked as `unmatched-skus` until the SKU is fixed. |
|
|
166
|
-
| `stockSync.enabled` | `true` | Sync stock from Print Trail: the scheduled job, `stock-level-updated` webhooks and refreshes after a stock shortfall or dispatch. Read when each run starts. |
|
|
167
|
-
| `reconcile.enabled` | `true` | Run the reconcile poll. Read when the job runs. |
|
|
168
|
-
| `reconcile.lookbackDays` | `7` | How far back the sweep looks for orders, and the reconcile poll's first window. |
|
|
169
|
-
| `autoComplete` | `true` | Complete the Medusa order once every Print Trail line has shipped. |
|
|
170
|
-
| `notifications.shipment` | `"print-trail-shipment"` | Template id for the per-parcel shipment email (`true`: the default id), or `false` for none. The plugin renders the email itself; the id is for a template-based provider (see Notifications). |
|
|
171
|
-
| `notifications.cancelTooLate` | `"print-trail-cancel-too-late"` | Template id for the admin email when a Medusa cancel comes too late for Print Trail (`true`: the default id), or `false` for none. Rendered by the plugin, as above. |
|
|
172
|
-
| `notifications.render` | `true` | Send the plugin's rendered email as `content`. `false`: `template` and `data` only, for a template-based provider such as SendGrid, which otherwise prefers `content`. |
|
|
173
|
-
| `notifications.adminEmail` | `[]` | Recipients of the cancel-too-late email: a list, or a comma-separated string. Empty means no email (a warning is logged). |
|
|
174
|
-
| `stockLocationIds` | every location | Stock locations whose levels the stock sync writes. Unset, a SKU's figure is written to every location that stocks it, with a `multi-location` warning. |
|
|
175
|
-
|
|
176
|
-
## Set up in the admin
|
|
177
|
-
|
|
178
|
-
1. **Connect.** Go to Settings → Print Trail and press **Test connection**. It checks the token and channel, saves the team code used for links into the Print Trail app, and registers this store's webhook URL, `{MEDUSA_BACKEND_URL}/hooks/print-trail`:
|
|
179
|
-
- Print Trail sends webhooks only to `https://` URLs of at most 200 characters on a public host, and since Print Trail API 1.2.0 refuses to register any other (the page shows Print Trail's reason). When the URL isn't `https`, or is a local, private or reserved address, the page asks before registering it (a development Print Trail or the bundled mock accepts one); a URL over 200 characters, with a user name or password, or with a `#fragment` is never registered. A local `http://` Medusa needs an HTTPS tunnel (ngrok, Cloudflare Tunnel and the like): set `MEDUSA_BACKEND_URL` to the tunnel's address.
|
|
180
|
-
- When the channel's webhook is registered at another address, the page shows "sending elsewhere" and asks before replacing it.
|
|
181
|
-
- Nothing registers on page load or on a schedule.
|
|
182
|
-
- Registering also fetches the channel's webhook secret from the Print Trail API and stores it, so the channel id and the API token are all a merchant copies. When the webhook is already registered here, Test connection posts the same address again to fetch the secret (Print Trail never changes it there). The page shows where the secret comes from: "From Print Trail", "From PRINT_TRAIL_WEBHOOK_SECRET" or "Not set".
|
|
183
|
-
2. **Shipping profile.** Create a shipping profile, for example "Print Trail", and put each Print Trail product in it. Medusa sets the profile on the product, and its variants inherit it. The plugin sends the order's items whose product is in a profile served by a Print Trail shipping option. Mixed baskets work, because Medusa asks for one shipping method per profile.
|
|
184
|
-
3. **Shipping option.** Create Print Trail shipping options on Settings → Print Trail → Shipping. Each option has a Print Trail method and a rate card (below).
|
|
185
|
-
- **The option's fulfilment set must be linked to a stock location.** Otherwise the plugin can't create Medusa fulfilments when Print Trail dispatches.
|
|
186
|
-
- With managed inventory, the item needs a reservation, which Medusa makes at checkout when the variant has an inventory level at a location the sales channel uses.
|
|
187
|
-
4. **SKUs.** A variant is matched to Print Trail by its SKU, case-insensitively. When Print Trail's SKU differs, set the variant metadata key `print_trail_sku` (the Print Trail panel on the variant page edits it). Saving a variant or moving a product into a Print Trail profile checks the SKU with Print Trail. Settings → Print Trail lists unmatched SKUs. A SKU is unmatched when Print Trail's stock list doesn't have it; a sold-out product is still listed, with 0. Print Trail's stock list also includes inactive products without marking them, so a variant matched to an inactive Print Trail product counts as matched and its stock is synced. The variant's SKU check shows it as Inactive, and Print Trail may refuse orders for it at confirm.
|
|
188
|
-
- **Sync stock now** on the same page runs a full stock sync at once, as the daily job does, and says whether it ran or was deferred: while another stock sync holds the lock, the request waits for the next sweep. With Print Trail API 1.3.0, whose stock list gives each product's status, a full stock sync also brings each variant's SKU check up to date (matched, pre-order or inactive), and a SKU Print Trail has started listing is checked again at the next sweep, which re-sends the orders waiting on it.
|
|
189
|
-
- Deleting a variant or a product removes its SKU checks, and their SKUs leave the unmatched list at once, unless another variant was checked with the same SKU. A deleted variant's SKU stays until the next full stock sync when the variant was never checked, when a stock sync was running as it was deleted, when many variants are deleted at once and the stock lock stays busy, and in a few other narrow cases. After an upgrade from 0.2.0, a SKU that 0.2.0 had listed as unmatched stays until the upgrade's one-off scan of variant metadata completes (the CHANGELOG's upgrade note). **Sync stock now** clears it straight away, once no other stock sync is running.
|
|
190
|
-
|
|
191
|
-
### The webhook secret
|
|
192
|
-
|
|
193
|
-
Print Trail signs every webhook with the channel's secret. The plugin gets it from the Print Trail API, as the response to registering the webhook (`POST /webhooks`), and stores it in `print_trail_state`, encrypted with a key derived from the API token. It never reaches the browser or a log.
|
|
194
|
-
|
|
195
|
-
- **Precedence:** the stored secret wins. `PRINT_TRAIL_WEBHOOK_SECRET` is used only while nothing is stored, which happens with a Print Trail API that doesn't return the secret yet. When both are set and differ, the settings page warns; remove the variable or update it. The two are never both accepted, so a secret that has been replaced stops working.
|
|
196
|
-
- **Rotating:** "Rotate secret" on the settings page asks Print Trail for a new secret and stores it. It is offered only while the channel sends its webhooks to this store. Deliveries Print Trail queued before the rotation are still signed with the old secret, so the old one is accepted for 10 more minutes. A delivery refused after that is caught up by the reconcile poll.
|
|
197
|
-
- **Rotated in the Print Trail app** (Settings → Channels, "Webhook secret"): webhooks are refused until someone presses Test connection, which fetches the new secret. The old one is accepted for 4 hours from then.
|
|
198
|
-
- **A new API token** makes the stored secret unreadable. The sweep fetches it again on its next run while the channel sends to this store (it never registers elsewhere); Test connection does the same at once.
|
|
199
|
-
|
|
200
|
-
### Rate cards
|
|
201
|
-
|
|
202
|
-
Each Print Trail shipping option stores its method and rate card in its `data`:
|
|
203
|
-
|
|
204
|
-
```json
|
|
205
|
-
{
|
|
206
|
-
"method": "tracked",
|
|
207
|
-
"currency_code": "gbp",
|
|
208
|
-
"bands": [
|
|
209
|
-
{ "max_weight": 1000, "prices": { "gbp": 3.95 } },
|
|
210
|
-
{ "max_weight": null, "prices": { "gbp": 6.5 } }
|
|
211
|
-
]
|
|
212
|
-
}
|
|
213
|
-
```
|
|
214
|
-
|
|
215
|
-
- **One currency per option.** Medusa's price calculation passes no currency to the provider, so the card names its own `currency_code`. A store selling in two currencies makes one option per currency. Each band's `prices` is keyed by currency and must include the card's `currency_code`. Prices in other currencies may sit alongside, and the shipping page keeps them.
|
|
216
|
-
- **Weights are in grams**, as Medusa stores them. `max_weight` is an inclusive upper limit. Bands must ascend, and only the last may be `null` (no limit).
|
|
217
|
-
- **Weight fallback.** A variant with no weight uses its product's weight. A weight of `0` is a real weight and is not replaced.
|
|
218
|
-
- **Prices** are major units, stored as entered (`3.95` is £3.95).
|
|
219
|
-
- **Validation.** `method` must be one the channel offers (`GET /shipping-methods`), or one of `standard`, `express`, `air` or `tracked` when Print Trail can't be reached. Cards are validated on the plugin's shipping page and on Medusa's own `/admin/shipping-options` routes.
|
|
220
|
-
|
|
221
|
-
#### Prices by country
|
|
222
|
-
|
|
223
|
-
A card can price some destinations differently. `countries` maps a country code to that country's own bands; any destination not listed, or a cart with no shipping address yet, uses `bands`:
|
|
224
|
-
|
|
225
|
-
```json
|
|
226
|
-
{
|
|
227
|
-
"method": "standard",
|
|
228
|
-
"currency_code": "gbp",
|
|
229
|
-
"bands": [
|
|
230
|
-
{ "max_weight": 250, "prices": { "gbp": 17.5 } },
|
|
231
|
-
{ "max_weight": 2000, "prices": { "gbp": 41.5 } }
|
|
232
|
-
],
|
|
233
|
-
"countries": {
|
|
234
|
-
"fr": [
|
|
235
|
-
{ "max_weight": 250, "prices": { "gbp": 8.25 } },
|
|
236
|
-
{ "max_weight": 2000, "prices": { "gbp": 20 } }
|
|
237
|
-
]
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
```
|
|
241
|
-
|
|
242
|
-
- **The destination** is the cart's shipping address country. One option can then serve a whole region, rather than one option and service zone per price.
|
|
243
|
-
- **Codes** are ISO 3166-1 alpha-2, as Medusa lists them (`gb`, not `uk`). Upper-case codes are accepted, and the plugin's routes store them in lower case. An unknown code, or the same code twice in different cases, is refused. Medusa's own `/admin/shipping-options` routes store the card as sent, so they refuse an upper-case code: Medusa would keep it beside a stored lower-case one.
|
|
244
|
-
- **Each country's bands** follow the same rules as `bands`: ascending, only the last may be `null`, and every band needs a price in the card's `currency_code`.
|
|
245
|
-
- **A weight limit applies per country.** A cart heavier than the last band of the list that applies gets no price, so checkout can't offer the option. A country's last band caps it even when the default bands go higher. Over the limit:
|
|
246
|
-
- `POST /store/shipping-options/:id/calculate` answers 400 (`not_allowed`, "No standard rate for 2400 g to France in GBP"), so a storefront that leaves out options it can't price never shows it.
|
|
247
|
-
- Adding the option as the cart's shipping method is refused (400, "do not have a price").
|
|
248
|
-
- If the cart already has the option and an added item takes it over the limit, the item is added and Medusa removes the shipping method; the customer chooses again.
|
|
249
|
-
- **Storefronts should treat a calculated option without an `amount` as unavailable, never as free.** A storefront that reads a missing amount as 0 would offer it for nothing.
|
|
250
|
-
- **The shipping page** lists each option's countries, collapsed, and keeps them when you edit the option. The edit drawer can remove a country, which is also how to clear one the card can't accept (a card shown as invalid). It doesn't edit a country's bands, and it refuses a new currency that some country has no prices in: for either, send the whole card, `countries` included, to `POST /admin/print-trail/shipping-options/:id`. That replaces the card, so a country left out is removed. Medusa's own `/admin/shipping-options/:id` merges the body's `data` into the stored card (objects key by key, arrays and `null` replaced), so it can add one country with `{ "countries": { "de": [...] } }` and remove one by sending it as `null` under exactly the code the card stores (a `null` naming no stored code is refused, since it would remove nothing); a `null` country is read as not listed. The plugin checks the card as it will be after that merge, so a change that would leave it invalid (a new `currency_code` that a listed country has no prices in, say) is refused.
|
|
251
|
-
|
|
252
|
-
### Needs attention
|
|
253
|
-
|
|
254
|
-
An order that needs a person shows an attention flag in the order's Print Trail panel and on the Needs attention page. Most flags clear themselves: a send block clears when the order sends, a retry flag on Resend, and a Print Trail state on the next sync that no longer shows it. **Mark as resolved** clears the ones nothing else will, such as a dispatch mismatch, "complete manually", a cancel that came too late, or a refund Print Trail may still ship.
|
|
255
|
-
|
|
256
|
-
Print Trail's final states can be marked as resolved too: returned (`pt-returned`), part returned (`pt-fulfilment-returned`), partly dispatched (`pt-partial`) and a failed fulfilment (`pt-fulfilment-failed`). Print Trail won't change these, so once you have dealt with one, the sync doesn't raise it again. Resolving one resolves every final state the order shows at that moment, so a returned order's returned parcels, or a partial order's failed parcel, aren't flagged next. Anything that reaches a final state later is still flagged: another parcel that fails or is returned, or the whole order returned after a part return. A higher flag that comes and goes, such as a hold, doesn't bring a resolved parcel back. Once Print Trail no longer reports a resolved state, the plugin forgets it, so it is flagged again if it ever comes back. The memory belongs to the Print Trail order: if the order is re-sent as a different Print Trail order, it starts afresh. A pre-order waiting on a product that isn't active at Print Trail (`pt-pre-order-inactive`) can be marked as resolved the same way, on its own: resolving it doesn't resolve a final state, nor the reverse.
|
|
73
|
+
There are two blocks because Medusa passes a plugin's options only to the modules inside it, and a fulfilment provider is registered under the Fulfillment Module instead. `printTrailOptionsFromEnv()` reads the credentials once, so both blocks get the same values.
|
|
257
74
|
|
|
258
|
-
|
|
75
|
+
Setting `providers` replaces Medusa's default list, so keep the manual provider listed or the store's manual shipping options stop working. If the store already has an `@medusajs/medusa/fulfillment` entry, add the Print Trail provider to it.
|
|
259
76
|
|
|
260
|
-
|
|
77
|
+
### 3. Set the environment variables
|
|
261
78
|
|
|
262
|
-
|
|
79
|
+
- `PRINT_TRAIL_BASE_URL`: `https://api.printtrail.com`. It has no default, so set it with the other two: a channel ID or token without it stops the plugin's module loading.
|
|
80
|
+
- `PRINT_TRAIL_CHANNEL_ID`: the store's Print Trail channel ID.
|
|
81
|
+
- `PRINT_TRAIL_API_TOKEN`: the channel's API token.
|
|
82
|
+
- `MEDUSA_BACKEND_URL`: this backend's public `https://` URL. Print Trail sends its webhooks to `{MEDUSA_BACKEND_URL}/hooks/print-trail`, and only to an `https://` URL on a public host, so a local store needs an HTTPS tunnel.
|
|
83
|
+
- `MEDUSA_ADMIN_URL`: the Medusa admin URL, for links from Print Trail back to the order.
|
|
84
|
+
- `PRINT_TRAIL_ADMIN_EMAIL`: who is emailed when a Medusa cancel comes too late for Print Trail. A comma-separated list is accepted.
|
|
263
85
|
|
|
264
|
-
|
|
86
|
+
The plugin reads the three `PRINT_TRAIL_` credentials itself; the other three are the names the config above passes through. Changing any of them later needs a restart.
|
|
265
87
|
|
|
266
|
-
|
|
267
|
-
- **The rest ships.** In a mixed order the in-stock parcels ship as normal. Print Trail releases the waiting part when the product goes live, and the order completes when the last parcel ships.
|
|
268
|
-
- **Live before the stock arrives.** The waiting fulfilment becomes an ordinary back-order ("Back-order: out of stock at Print Trail"), which is attention. Print Trail signals this only through its stock update, which it batches over up to ten minutes; the next sweep then re-reads the order, so it usually shows within 15 minutes. If the product is then put back on pre-order, the fulfilment waits for the release again and the attention clears the same way.
|
|
269
|
-
- **Only going live releases.** A product made inactive at Print Trail while orders wait keeps them waiting there, whatever stock arrives, until it is made active. The fulfilment reads "Pre-order (product not active)" and the order gets a low-priority flag, "A pre-order product isn't active at Print Trail, so the order waits". Mark it as resolved if that's expected; it stays resolved until Print Trail no longer reports it.
|
|
270
|
-
- **Held after release.** When Print Trail can't reach the warehouse to send a released fulfilment, it holds it, and may try again for up to an hour (a refusal from the warehouse waits for someone at Print Trail). The order shows "A fulfilment is holding at Print Trail" meanwhile, and the flag may clear on its own.
|
|
271
|
-
- **Stock.** For an unlimited pre-order product, the stock sync turns backorders on (so it can be bought at 0 stock) and turns them off again once the product goes live, but only where the plugin turned them on: a backorder flag the store set itself is left alone. A pre-order product with a limit has what remains of the limit as its available stock, with backorders off, so the store stops selling at the limit. (Print Trail's remaining figure already counts the confirmed pre-orders, so the stock written adds back Medusa's own reservations for them.) With several stock locations, the allowance goes to one level only (Medusa adds availability up across locations): set `stockLocationIds` to choose it, or the plugin uses the first location by id. The variant page shows the pre-order status, the expected date, and the limit with what's left.
|
|
272
|
-
- **In the storefront.** The stock sync writes `print_trail_pre_order` into the metadata of every variant it manages: Print Trail's publication date (`"2027-03-04"`) while the product is on pre-order, or `true` when Print Trail has no date, and nothing once it isn't. It follows Print Trail's status alone, so a limited pre-order and a variant whose backorders the store turned on itself carry it too. Variant metadata is public on store routes: the store product routes' default fields (`*variants`) return it, as does a cart's `*items.variant`, so a storefront that keeps the default fields already has it, and shows its pre-order badge, button and date from it. From Medusa 2.21 a store route silently drops a requested field that isn't on its allowed list, and `variants.metadata` isn't on the product routes' list: `+variants.metadata` is harmless but adds nothing, and a field list that replaces the defaults (fields without `+` or `*`) must include `*variants`. Alternatively the store allows the field itself with `allowFields("variants.metadata")` from `@medusajs/framework/http`, in a middleware with `matcher: "/store/products*"` and no `method` (a method-scoped middleware runs after the query is checked). What remains of a limit needs no field of its own: it is the variant's available stock (`inventory_quantity`). The flag is written by the stock sync, so it follows a status or date change at Print Trail within a webhook-driven sync (the change sends `stock-level-updated`, which runs one; the daily run catches a missed webhook), only for variants with managed inventory in a Print Trail shipping profile; a variant that leaves those loses it at the next full sync. It needs Print Trail API 1.3.0: with an older API the flag is left as it is.
|
|
273
|
-
- **The pre-order's parcel email.** The shipment email for the released parcel says so: "Your pre-order from order #{display_id} has shipped" (Notifications, below). In a mixed order the in-stock parcel gets the ordinary email first.
|
|
274
|
-
- **Edits and cancels.** While the pre-order part waits at Print Trail, the order can still be edited and cancelled (a cancel is immediate). Once Print Trail has sent any part to the warehouse, edits are refused as for any order in progress, and a cancel goes through the warehouse. An edit past a pre-order limit (or past stock held by other orders) is refused with Print Trail's message, and the store's stock for those SKUs is refreshed.
|
|
275
|
-
- **Back-order warehouses.** A confirm for an active product Print Trail has none of, in a warehouse that allows back-orders, is accepted as a back-order: the order is confirmed with back-order attention, no retry, and the store's stock for those SKUs is refreshed.
|
|
88
|
+
### 4. Run the migrations and start Medusa
|
|
276
89
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
Both emails go through Medusa's Notification Module, so the store needs a notification provider on the `email` channel (for example SendGrid, Resend or `@medusajs/medusa/notification-local` for testing).
|
|
280
|
-
|
|
281
|
-
**The plugin renders both emails.** Each notification carries `content: { subject, html, text }`, plain-styled, with every value HTML-escaped and only `http(s)` URLs made into links. A provider that sends `content` (an SMTP or Resend provider, say) needs nothing more: the store doesn't supply templates. Each notification also carries `template` (the ids below, the options' defaults) and `data` (below), for a store that renders its own. Medusa's SendGrid provider sends `content` whenever it is present and ignores the template, so a store with its own SendGrid templates sets `notifications.render: false`: the plugin then sends `template` and `data` only, and SendGrid uses the template with that id. Set an email's option to `false` to turn it off. Medusa doesn't store `content` with the notification, so a notification resent from Medusa later carries only `template` and `data`.
|
|
282
|
-
|
|
283
|
-
**`print-trail-shipment`** (`notifications.shipment`). One email per dispatched Print Trail parcel, to the order's email, sent only for fulfilments the plugin created and only once a tracking number exists. Tracking that arrives later is emailed then. The plugin's email: "Your order #{display_id} is on its way", the carrier, the items in the parcel and the tracking links. A parcel that waited at Print Trail for its pre-order release is worded as the pre-order: "Your pre-order from order #{display_id} has shipped", headed "Your pre-order from order #{display_id} is on its way", "Your pre-order from order #{display_id} has been released and dispatched by {carrier}." (`pre_order: true`). The plugin knows a parcel waited because it records each Print Trail fulfilment it sees waiting for release (from the create and confirm responses and every sync), since by dispatch Print Trail no longer says so. The template receives:
|
|
284
|
-
|
|
285
|
-
```ts
|
|
286
|
-
{
|
|
287
|
-
order: { id: string, display_id: number | string | null },
|
|
288
|
-
print_trail_order_number: string | null,
|
|
289
|
-
carrier: string | null,
|
|
290
|
-
items: { line_item_id: string | null, title: string | null, quantity: number }[],
|
|
291
|
-
tracking: { number: string, url: string | null }[],
|
|
292
|
-
pre_order: boolean, // the parcel waited for its pre-order release
|
|
293
|
-
}
|
|
294
|
-
```
|
|
295
|
-
|
|
296
|
-
**`print-trail-cancel-too-late`** (`notifications.cancelTooLate`). Sent when an order is cancelled in Medusa but Print Trail has already dispatched some or all of it. One email goes to each `notifications.adminEmail` address, and the order also gets a timeline note and the `cancel-too-late` attention flag. The plugin's email: "Order #{display_id} was cancelled but has already shipped" (the order id without a display id), the Print Trail order number linked into the Print Trail app, what happened, each fulfilment with its status and tracking (or "no tracking"), the next step (contact the customer, then refund or arrange a return), and links to the order in Medusa and in Print Trail. The template receives:
|
|
297
|
-
|
|
298
|
-
```ts
|
|
299
|
-
{
|
|
300
|
-
order: { id: string, display_id: number | string | null },
|
|
301
|
-
print_trail_order_number: string,
|
|
302
|
-
pt_url: string | null, // the order in the Print Trail app
|
|
303
|
-
admin_order_url: string | null, // null without adminUrl
|
|
304
|
-
why: "dispatched" | "complete" | "partial",
|
|
305
|
-
fulfilments: {
|
|
306
|
-
number: string | null,
|
|
307
|
-
status: string,
|
|
308
|
-
tracking: { number: string, url: string, carrier: string | null }[],
|
|
309
|
-
}[],
|
|
310
|
-
}
|
|
311
|
-
```
|
|
312
|
-
|
|
313
|
-
Medusa refuses to cancel an order that has live fulfilments, or once it is completed. So "cancel too late" arises only in the gap between Print Trail dispatching and the plugin creating the Medusa fulfilment. After that, use Medusa's return flow.
|
|
314
|
-
|
|
315
|
-
A failed email is logged and not retried.
|
|
316
|
-
|
|
317
|
-
## Production checklist
|
|
318
|
-
|
|
319
|
-
Apply this to any store before taking live orders.
|
|
320
|
-
|
|
321
|
-
- **Use Redis locking if you run more than one process.** A separate server and worker count as two. The plugin's per-order lock is what stops two processes sending one order twice, or creating duplicate fulfilments, shipments and emails. Medusa's default lock is in-memory, which works within one process only. A run with two processes and no shared lock posted one order twice. Register the Redis locking provider:
|
|
322
|
-
|
|
323
|
-
```ts
|
|
324
|
-
modules: [
|
|
325
|
-
{
|
|
326
|
-
resolve: "@medusajs/medusa/locking",
|
|
327
|
-
options: {
|
|
328
|
-
providers: [
|
|
329
|
-
{
|
|
330
|
-
resolve: "@medusajs/medusa/locking-redis",
|
|
331
|
-
id: "locking-redis",
|
|
332
|
-
is_default: true,
|
|
333
|
-
options: {
|
|
334
|
-
redisUrl: process.env.LOCKING_REDIS_URL,
|
|
335
|
-
},
|
|
336
|
-
},
|
|
337
|
-
],
|
|
338
|
-
},
|
|
339
|
-
},
|
|
340
|
-
]
|
|
341
|
-
```
|
|
342
|
-
|
|
343
|
-
The Postgres locking provider is unsupported: it never reclaims an expired lock with no owner, and its insert race lets two holders through. The plugin checks the locking provider at startup. It logs an error for the Postgres provider, and the settings page warns when locking is in-memory or Postgres.
|
|
344
|
-
|
|
345
|
-
With Redis locking, a lock held by a process that stops mid-run (a restart or a crash) stays until it expires, and that work waits until then: the SKU check for up to 5 minutes, an order's sends and updates for up to 10, and the stock sync for up to 15. Nothing is lost: queued SKU checks, the order and any stock sync requested meanwhile run once the lock has expired.
|
|
346
|
-
- **Use a Redis event bus** (`@medusajs/medusa/event-bus-redis`). The in-memory bus can drop events across restarts. The sweep finds orders whose `order.placed` never arrived, but only within `reconcile.lookbackDays`.
|
|
347
|
-
- **Switch payments and Print Trail credentials together.** Live payments with staging credentials send real orders nowhere. Test payments with production credentials send test orders to be made.
|
|
348
|
-
- **Clear or cancel test orders first.** The sweep sends any uncancelled order with Print Trail lines and no Print Trail order from the last `reconcile.lookbackDays` days. Test orders left open when production credentials go in will be sent.
|
|
349
|
-
- **Set `PRINT_TRAIL_ADMIN_EMAIL`**, so someone hears about a cancel that came too late.
|
|
350
|
-
- **Register the webhook** with Test connection on the production backend URL. Check that the settings page shows the webhook secret "From Print Trail". Only if it says "Not set" (a Print Trail API that doesn't return the secret yet) copy the production channel's secret from the Print Trail app into `PRINT_TRAIL_WEBHOOK_SECRET` and restart.
|
|
351
|
-
- **Admin access.** The plugin has no permissions of its own. Any admin user can use its pages and admin routes.
|
|
352
|
-
|
|
353
|
-
## Developer surface
|
|
354
|
-
|
|
355
|
-
### Package entry points
|
|
356
|
-
|
|
357
|
-
| Import | What it holds |
|
|
358
|
-
|---|---|
|
|
359
|
-
| `medusa-plugin-print-trail/config` | `printTrailOptionsFromEnv`, `resolvePluginOptions` and the option types, for `medusa-config.ts`. |
|
|
360
|
-
| `medusa-plugin-print-trail/providers/print-trail` | The fulfilment provider, for the fulfilment module's `providers`. |
|
|
361
|
-
| `medusa-plugin-print-trail/modules/printTrail` | The module (`PRINT_TRAIL_MODULE`, and `linkable` for links of your own). |
|
|
362
|
-
| `medusa-plugin-print-trail/workflows` | The workflows, their ids and the hook input types (below). |
|
|
363
|
-
| `medusa-plugin-print-trail/stock` | `requestStockSync`, the entry point every stock trigger uses (it takes the stock lock, and a held lock defers the request to the sweep), and `describeStockResult`. |
|
|
364
|
-
| `medusa-plugin-print-trail/shipping-options` | `printTrailShippingMethods`, `validateRateCard` and `printTrailShippingOptionInput`, to create Print Trail shipping options from a script. |
|
|
365
|
-
| `medusa-plugin-print-trail/admin` | The admin extensions, for Medusa's admin build. |
|
|
366
|
-
| `medusa-plugin-print-trail/package.json` | The package's own `package.json`. |
|
|
367
|
-
|
|
368
|
-
`medusa-plugin-print-trail/.medusa/server/src/modules/printTrail` is also exported, because Medusa loads the module from that path; don't import it. Nothing else in the package can be imported: a file that isn't listed here can change in any release.
|
|
369
|
-
|
|
370
|
-
TypeScript declarations ship for every entry point but `/admin`. They resolve with `moduleResolution` `node16`, `nodenext` or `bundler`, not `node10`, which can't resolve Medusa's own subpaths either.
|
|
371
|
-
|
|
372
|
-
A store's own job asking for a SKU-only sync, and a script creating a Print Trail shipping option:
|
|
373
|
-
|
|
374
|
-
```ts
|
|
375
|
-
// src/jobs/refresh-new-titles.ts
|
|
376
|
-
import type { MedusaContainer } from "@medusajs/framework/types"
|
|
377
|
-
import { ContainerRegistrationKeys } from "@medusajs/framework/utils"
|
|
378
|
-
import { describeStockResult, requestStockSync } from "medusa-plugin-print-trail/stock"
|
|
379
|
-
|
|
380
|
-
export default async function refreshNewTitles(container: MedusaContainer) {
|
|
381
|
-
const result = await requestStockSync(container, { trigger: "schedule", skus: ["9781234567897"] })
|
|
382
|
-
container.resolve(ContainerRegistrationKeys.LOGGER).info(`New titles' stock: ${describeStockResult(result)}`)
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
export const config = { name: "refresh-new-titles", schedule: "0 * * * *" }
|
|
386
|
-
```
|
|
387
|
-
|
|
388
|
-
```ts
|
|
389
|
-
// src/scripts/create-print-trail-option.ts: npx medusa exec ./src/scripts/create-print-trail-option.ts
|
|
390
|
-
import type { ExecArgs } from "@medusajs/framework/types"
|
|
391
|
-
import { createShippingOptionsWorkflow } from "@medusajs/medusa/core-flows"
|
|
392
|
-
import {
|
|
393
|
-
printTrailShippingMethods,
|
|
394
|
-
printTrailShippingOptionInput,
|
|
395
|
-
validateRateCard,
|
|
396
|
-
} from "medusa-plugin-print-trail/shipping-options"
|
|
397
|
-
|
|
398
|
-
export default async function createPrintTrailOption({ container }: ExecArgs) {
|
|
399
|
-
const { codes } = await printTrailShippingMethods(container)
|
|
400
|
-
const checked = validateRateCard(
|
|
401
|
-
{ method: "tracked", currency_code: "gbp", bands: [{ max_weight: 1000, prices: { gbp: 3.95 } }, { max_weight: null, prices: { gbp: 6.5 } }] },
|
|
402
|
-
codes
|
|
403
|
-
)
|
|
404
|
-
if (!checked.ok) throw new Error(checked.issues.map((i) => `${i.path}: ${i.message}`).join("; "))
|
|
405
|
-
const option = printTrailShippingOptionInput(
|
|
406
|
-
{ name: "Print Trail Tracked", service_zone_id: "serzo_…", shipping_profile_id: "sp_…" },
|
|
407
|
-
checked.card
|
|
408
|
-
)
|
|
409
|
-
await createShippingOptionsWorkflow(container).run({ input: [option] })
|
|
410
|
-
}
|
|
90
|
+
```bash
|
|
91
|
+
npx medusa db:migrate
|
|
411
92
|
```
|
|
412
93
|
|
|
413
|
-
|
|
94
|
+
They create the plugin's tables and its link to orders. Then start Medusa, or restart it if it's running, so it reads the variables.
|
|
414
95
|
|
|
415
|
-
###
|
|
96
|
+
### 5. Connect in the admin
|
|
416
97
|
|
|
417
|
-
|
|
98
|
+
1. Go to Settings → Print Trail and press **Test connection**. It checks the token and channel, registers the store's webhook with Print Trail and fetches the webhook secret.
|
|
99
|
+
2. Put the products Print Trail fulfils in a shipping profile of their own.
|
|
100
|
+
3. On Settings → Print Trail, follow the **Print Trail shipping options** link and create a Print Trail shipping option for that profile, in a service zone whose fulfilment set is linked to a stock location.
|
|
418
101
|
|
|
419
|
-
|
|
102
|
+
Variants are matched to Print Trail by SKU. Where Print Trail's SKU differs, set it in the Print Trail panel on the variant page.
|
|
420
103
|
|
|
421
|
-
|
|
104
|
+
Before taking live orders, cancel any open test orders (the plugin sends open orders with Print Trail lines from the last 7 days), and switch payments and Print Trail credentials to live together.
|
|
422
105
|
|
|
423
|
-
|
|
424
|
-
|---|---|
|
|
425
|
-
| `sendOrderToPrintTrailWorkflow` | Sends an order's Print Trail lines as a draft, then confirms it when the gate allows. Input: `{ order_id, sequence?, source_ref?, reissue_of?, lock_timeout? }`. |
|
|
426
|
-
| `syncPrintTrailOrderWorkflow` | Re-fetches a Print Trail order and applies it: status, one Medusa fulfilment and shipment per dispatched parcel, completion, attention. |
|
|
427
|
-
| `cancelPrintTrailOrderWorkflow` | Carries a Medusa cancel to Print Trail. |
|
|
428
|
-
| `amendPrintTrailOrderWorkflow` | Carries an order edit to Print Trail (`PUT …/lines` once confirmed, a re-sent draft before). |
|
|
429
|
-
| `recordPrintTrailOrderChangeWorkflow` | Records an address change, refund or return on the timeline. |
|
|
430
|
-
| `syncPrintTrailStockWorkflow` | Syncs stock levels from Print Trail. It fails while another stock sync holds the lock; `requestPrintTrailStockSyncWorkflow` and `requestStockSync` (`/stock`) defer instead. |
|
|
431
|
-
| `requestPrintTrailStockSyncWorkflow` | Sync stock now: a full sync, or only `{ skus }`, through `requestStockSync`. Its result's `status` is `completed`, `deferred` (to the next sweep), `disabled` or `failed`. |
|
|
432
|
-
| `testPrintTrailConnectionWorkflow` | Test connection. |
|
|
433
|
-
| `registerPrintTrailWebhookWorkflow` | Registers the webhook URL. |
|
|
434
|
-
| `resetPrintTrailRetryWorkflow` | Resets a capped retry (used by Resend). |
|
|
435
|
-
| `dismissPrintTrailAttentionWorkflow` | Clears an attention code an admin has dealt with. A final Print Trail code, or complete-manually, stays dismissed while it still applies. |
|
|
106
|
+
## Documentation
|
|
436
107
|
|
|
437
|
-
|
|
108
|
+
- **Guide:** [docs.printtrail.com/integrations/medusa](https://docs.printtrail.com/integrations/medusa/), for setting the plugin up and using it day to day.
|
|
109
|
+
- **Reference:** [docs.printtrail.com/integrations/medusa-reference](https://docs.printtrail.com/integrations/medusa-reference/), for every option, rate cards, pre-orders, notifications, the workflows and hooks, events, jobs, API routes and troubleshooting.
|
|
438
110
|
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
**`filterLines`** holds lines back, for example unreleased pre-orders. It receives `{ order, lines, sequence, source_ref }` and returns `{ lines }`. Each line is `{ lineItemId, sku, quantity, method }`. **Register the same handler on both workflows**, or an order edit will send the lines the send held back:
|
|
444
|
-
|
|
445
|
-
```ts
|
|
446
|
-
import { StepResponse } from "@medusajs/framework/workflows-sdk"
|
|
447
|
-
import {
|
|
448
|
-
amendPrintTrailOrderWorkflow,
|
|
449
|
-
sendOrderToPrintTrailWorkflow,
|
|
450
|
-
type FilterLinesHookInput,
|
|
451
|
-
} from "medusa-plugin-print-trail/workflows"
|
|
452
|
-
|
|
453
|
-
const holdUnreleased = async ({ order, lines }: FilterLinesHookInput) => {
|
|
454
|
-
const released = (itemId: string) => {
|
|
455
|
-
const item = order.items?.find((i) => i?.id === itemId)
|
|
456
|
-
const date = item?.variant?.metadata?.release_date
|
|
457
|
-
return typeof date !== "string" || new Date(date) <= new Date()
|
|
458
|
-
}
|
|
459
|
-
return new StepResponse({ lines: lines.filter((l) => released(l.lineItemId)) })
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
sendOrderToPrintTrailWorkflow.hooks.filterLines(holdUnreleased)
|
|
463
|
-
amendPrintTrailOrderWorkflow.hooks.filterLines(holdUnreleased)
|
|
464
|
-
```
|
|
465
|
-
|
|
466
|
-
Held lines are recorded on the order's Print Trail row. To send them once released, run `sendOrderToPrintTrailWorkflow` with just `{ order_id }`. Before the first Print Trail order is confirmed, that re-sends it with the released lines. After, it sends them as a new `-n` order. The plugin doesn't release lines on its own. The sweep only re-checks orders within `reconcile.lookbackDays`, so a store with longer pre-orders needs its own release job (a scheduled job that runs the workflow for orders whose lines are now released).
|
|
467
|
-
|
|
468
|
-
**`orderNumber`** changes the order number of a new Print Trail order. It receives `{ order, sequence, source_ref, orderNumber }`, where `orderNumber` is the default, and returns `{ orderNumber }` or a string. An empty value, one over 100 characters, or one another Print Trail order of the same Medusa order already has falls back to the default, with a warning.
|
|
469
|
-
|
|
470
|
-
```ts
|
|
471
|
-
sendOrderToPrintTrailWorkflow.hooks.orderNumber(async ({ order, sequence }) =>
|
|
472
|
-
new StepResponse({ orderNumber: `WEB-${order.display_id}${sequence > 1 ? `-${sequence}` : ""}` })
|
|
473
|
-
)
|
|
474
|
-
```
|
|
475
|
-
|
|
476
|
-
**`payload`** is a last chance to change the create request. It receives `{ order, sequence, source_ref, payload }` and returns `{ payload }` or the fields themselves. Only `shippingDetails`, `purchaseOrder`, `printTrailOrderNumber` and `orderUrl` are taken from the result. The plugin's order number, lines, uuid and flags always win.
|
|
477
|
-
|
|
478
|
-
```ts
|
|
479
|
-
sendOrderToPrintTrailWorkflow.hooks.payload(async ({ order, payload }) =>
|
|
480
|
-
new StepResponse({ payload: { ...payload, purchaseOrder: String(order.metadata?.po_number ?? "") || undefined } })
|
|
481
|
-
)
|
|
482
|
-
```
|
|
483
|
-
|
|
484
|
-
### Events
|
|
485
|
-
|
|
486
|
-
Emitted on Medusa's event bus. The `print_trail.order.*` events except `copy_seen` carry `{ order_id, print_trail_order_id, pt_uuid }`, plus the fields shown.
|
|
487
|
-
|
|
488
|
-
| Event | When |
|
|
489
|
-
|---|---|
|
|
490
|
-
| `print_trail.order.sent` | A draft was created in Print Trail. |
|
|
491
|
-
| `print_trail.order.confirmed` | The Print Trail order was confirmed. |
|
|
492
|
-
| `print_trail.order.blocked` | Sending or confirming was refused (`reason`, for example `unmatched-skus` or `stock-shortfall`). |
|
|
493
|
-
| `print_trail.order.dispatched` | A Print Trail parcel was dispatched and recorded in Medusa: `{ pt_fulfilment_uuid, fulfillment_id, was_pre_order }` (`was_pre_order`: it waited for its pre-order release). |
|
|
494
|
-
| `print_trail.order.cancelled` | The Print Trail order was cancelled. |
|
|
495
|
-
| `print_trail.order.attention` | The order needs attention (`attention`, the code). |
|
|
496
|
-
| `print_trail.order.stock_held` | Print Trail refused an order edit for stock (`skus`). The plugin refreshes those SKUs' stock. |
|
|
497
|
-
| `print_trail.order.copy_seen` | A webhook named a Print Trail copy of a known order (`{orderNumber}-COPY`): `{ order_id, print_trail_order_id, copy_uuid, copy_order_number }`. |
|
|
498
|
-
| `print_trail.webhook.received` | A signed order webhook for a known order was accepted: `{ order_id, print_trail_order_id, notification_type }`. The plugin re-fetches the order on it. |
|
|
499
|
-
| `print_trail.stock.changed` | A `stock-level-updated` webhook was accepted: `{ changed_at, activated_skus, held_skus }` (the SKUs now active, and those on pre-order or inactive with their status). The plugin runs a stock sync on it and re-reads the orders whose waiting fulfilments those changes move. |
|
|
500
|
-
|
|
501
|
-
### Jobs
|
|
502
|
-
|
|
503
|
-
`print-trail-sweep`, `print-trail-reconcile` and `print-trail-stock-sync`. See [Job schedules](#job-schedules).
|
|
504
|
-
|
|
505
|
-
### API routes
|
|
506
|
-
|
|
507
|
-
The webhook receiver is `POST /hooks/print-trail`. It checks the HMAC-SHA256 of the raw body against the `Signature` header and fails closed (401). A valid delivery gets a 202 at once. The plugin then re-fetches the order from Print Trail and never applies the webhook body itself.
|
|
508
|
-
|
|
509
|
-
Admin routes, all under `/admin/print-trail`:
|
|
510
|
-
|
|
511
|
-
| Route | Methods | Purpose |
|
|
512
|
-
|---|---|---|
|
|
513
|
-
| `connection` | GET, POST | Connection status; Test connection. |
|
|
514
|
-
| `webhook` | POST | Register the webhook (`{ force: true }` after the page's question). |
|
|
515
|
-
| `webhook/rotate` | POST | Rotate the webhook secret (only while the channel sends here). |
|
|
516
|
-
| `shipping-options` | GET, POST | List and create Print Trail shipping options. |
|
|
517
|
-
| `shipping-options/:id` | POST, DELETE | Update and delete one. A `rate_card` replaces the stored card, `countries` included. |
|
|
518
|
-
| `orders/:id` | GET | The order's Print Trail rows, fulfilments and timeline. |
|
|
519
|
-
| `orders/:id/refresh` | POST | Re-fetch from Print Trail. |
|
|
520
|
-
| `orders/:id/resend` | POST | Resend or retry, including "Resend as a new Print Trail order". |
|
|
521
|
-
| `orders/:id/dismiss` | POST | Clear an attention code (Mark as resolved). |
|
|
522
|
-
| `attention` | GET | Orders that need attention. |
|
|
523
|
-
| `sku-health` | GET | Unmatched SKUs and the last stock run. |
|
|
524
|
-
| `stock-sync` | POST | Sync stock now. Takes no body; returns `{ status, summary }`. |
|
|
525
|
-
| `variants/:id` | GET | A variant's SKU check. |
|
|
526
|
-
|
|
527
|
-
## Troubleshooting
|
|
528
|
-
|
|
529
|
-
- **`medusa db:migrate` hangs after "Running migrations...".** A Unix-socket `DATABASE_URL` such as `postgres:///db?host=/var/run/postgresql` causes it. Use a password over TCP: `postgres://user:password@127.0.0.1:5432/db`.
|
|
530
|
-
- **A new store from `create-medusa-app@2.17.2` breaks.** It produces a template for a newer Medusa. Its `react-router-dom` is covered by the next item; also:
|
|
531
|
-
- remove the `configureStoreSearch` store-search middleware from `src/api/middlewares.ts`;
|
|
532
|
-
- remove the `Modules.SEARCH` step from the seed script.
|
|
533
|
-
- **Every plugin link in the admin crashes with a null router context.** The admin bundler leaves `react-router-dom` to the store, so the store's copy must have the dashboard's major version: 6.x on Medusa 2.17 and 2.18 (pin `6.30.4`; a `create-medusa-app@2.17.2` store starts on 7.x), 7.x from 2.19 (`7.18.2` on 2.22.1). A copy of the other major, installed or hoisted, breaks every plugin link.
|
|
534
|
-
- **Creating a calculated option through Medusa's own `POST /admin/shipping-options` fails.** Medusa 2.17.2 needs `prices: []` even when `price_type` is `calculated`. The plugin's shipping page sends it.
|
|
535
|
-
- **Dispatch doesn't create a Medusa fulfilment.** Check that the shipping option's fulfilment set is linked to a stock location, that managed-inventory variants have a level there, and that the order isn't cancelled. The order's Print Trail timeline names the refusal.
|
|
536
|
-
- **"Reconnect Print Trail".** Print Trail answered 401: the API token or channel ID is wrong, or the token was revoked. Since Print Trail API 1.2.0 an unknown channel ID gets the same 401 as a bad token. Check `PRINT_TRAIL_CHANNEL_ID`, regenerate the token in the Print Trail app if needed, set `PRINT_TRAIL_API_TOKEN`, restart, and press Test connection. While the 401 lasts, the sweep and reconcile make one check per run instead of a request per order: Print Trail allows 60 failed requests a minute per IP address, then refuses everything from it for the rest of the minute.
|
|
537
|
-
- **"The Print Trail channel is inactive".** Print Trail answered 403 `channel-inactive` (API 1.2.0): the channel was set inactive in Print Trail, which refuses every request from it and sends it no webhooks. Orders, cancels and edits wait with that attention, costing no retry attempt. Make the channel active in Print Trail (Settings → Channels) and press Test connection; the waiting work runs on the next sweep (within about 15 minutes). A 401 waits the same way. A 403 saying "This team is inactive" (the team can still read and cancel, not send or confirm) or that the token belongs to the Standard channel (create a dedicated channel and token) shows Print Trail's own words, and is not retried: fix it in Print Trail, then Resend.
|
|
538
|
-
- **429 Too Many Requests.** Print Trail limits each token (600 requests a minute) and failed requests per IP address. The plugin waits a short `Retry-After` out, otherwise stops sending until the wait is over and leaves the work to the stored retries and the sweep. Repeated 429s usually mean a wrong token or channel ID on this server or another one behind the same address.
|
|
539
|
-
- **"Print Trail reference …" in an error.** Print Trail failed unexpectedly (500). The error keeps Print Trail's `reference`: quote it to Print Trail support. The plugin retries as usual.
|
|
540
|
-
- **Webhook registration refused (422).** Print Trail API 1.2.0 accepts only an `https://` URL of at most 200 characters on a public host that resolves, and re-checks it before every delivery (redirects aren't followed). A webhook registered earlier at an `http://` or private address no longer receives anything: put the store behind HTTPS (or an HTTPS tunnel for a local store), set `MEDUSA_BACKEND_URL` and press Test connection.
|
|
541
|
-
- **"The order is over Print Trail's limits".** Print Trail API 1.2.0 refuses an order number, `printTrailOrderNumber` or SKU over 255 characters and more than 500 lines, and the Print Trail API takes only whole-number quantities, while Medusa allows a fractional one (a product sold by weight, say). The order isn't sent, and a fractional quantity is never rounded; shorten the value (the `orderNumber` or `payload` hook, or `print_trail_sku`), split the order or correct the quantity with an order edit, then Resend. An order edit that leaves a fractional quantity on a sent order isn't sent to Print Trail either, and the order's timeline says why.
|
|
542
|
-
|
|
543
|
-
## Removing the plugin
|
|
544
|
-
|
|
545
|
-
1. Remove the `plugins` entry and the Print Trail provider from the fulfilment providers in `medusa-config.ts`. Keep the manual provider. Delete or replace any shipping options that use `print-trail_print-trail` first.
|
|
546
|
-
2. If you don't want the data, drop the plugin's tables: `print_trail_order`, `print_trail_order_event`, `print_trail_state`, `print_trail_variant`, and the order link table (Medusa names it `printtrail_print_trail_order_order_order`; `\dt *print_trail_order*` in psql lists it).
|
|
547
|
-
3. Remove the webhook from the channel in the Print Trail app.
|
|
548
|
-
|
|
549
|
-
Fulfilments, shipments and stock levels the plugin wrote into Medusa stay.
|
|
111
|
+
- [Changelog](https://github.com/consilience/medusa-plugin-print-trail/blob/main/CHANGELOG.md), with each release's upgrade notes.
|
|
112
|
+
- [Contributing](https://github.com/consilience/medusa-plugin-print-trail/blob/main/CONTRIBUTING.md): developing and testing the plugin.
|
|
113
|
+
- Problems and requests: [GitHub issues](https://github.com/consilience/medusa-plugin-print-trail/issues).
|
|
550
114
|
|
|
551
115
|
## Licence
|
|
552
116
|
|
|
@@ -557,7 +121,3 @@ The plugin is licensed under the [PolyForm Shield License 1.0.0](https://polyfor
|
|
|
557
121
|
- You can't use it to provide a product that competes with it, such as a Medusa integration for another fulfilment service built from this code.
|
|
558
122
|
|
|
559
123
|
The noncompete makes it a source-available licence, not an open-source one. This is a summary; the terms in `LICENSE` decide.
|
|
560
|
-
|
|
561
|
-
## Changelog
|
|
562
|
-
|
|
563
|
-
See [CHANGELOG.md](CHANGELOG.md).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "medusa-plugin-print-trail",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "Print Trail fulfilment for Medusa v2: orders sent automatically, stock levels synced and tracking sent back to Medusa.",
|
|
5
5
|
"author": "Consilience Media Limited",
|
|
6
6
|
"license": "SEE LICENSE IN LICENSE",
|