medusa-plugin-foundry-ims 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Plugin config resolver. Phase 1 reads from env vars only — Phase 2
3
+ * adds an admin-widget-backed config module that takes precedence.
4
+ *
5
+ * Required: FOUNDRY_API_KEY + FOUNDRY_CHANNEL_ID
6
+ * Optional: FOUNDRY_API_URL (defaults to https://api.foundryims.com)
7
+ *
8
+ * If required vars are missing, the plugin becomes a no-op and logs a
9
+ * one-time warning at startup. We never throw — a missing config must
10
+ * not break the merchant's order flow.
11
+ */
12
+ export interface FoundryConfig {
13
+ apiUrl: string;
14
+ apiKey: string;
15
+ channelId: string;
16
+ }
17
+ export declare function resolveConfig(): FoundryConfig | null;
@@ -0,0 +1,30 @@
1
+ "use strict";
2
+ /**
3
+ * Plugin config resolver. Phase 1 reads from env vars only — Phase 2
4
+ * adds an admin-widget-backed config module that takes precedence.
5
+ *
6
+ * Required: FOUNDRY_API_KEY + FOUNDRY_CHANNEL_ID
7
+ * Optional: FOUNDRY_API_URL (defaults to https://api.foundryims.com)
8
+ *
9
+ * If required vars are missing, the plugin becomes a no-op and logs a
10
+ * one-time warning at startup. We never throw — a missing config must
11
+ * not break the merchant's order flow.
12
+ */
13
+ Object.defineProperty(exports, "__esModule", { value: true });
14
+ exports.resolveConfig = resolveConfig;
15
+ let warnedMissing = false;
16
+ function resolveConfig() {
17
+ const apiUrl = (process.env.FOUNDRY_API_URL || 'https://api.foundryims.com').replace(/\/$/, '');
18
+ const apiKey = process.env.FOUNDRY_API_KEY;
19
+ const channelId = process.env.FOUNDRY_CHANNEL_ID;
20
+ if (!apiKey || !channelId) {
21
+ if (!warnedMissing) {
22
+ // eslint-disable-next-line no-console
23
+ console.warn('[medusa-plugin-foundry-ims] FOUNDRY_API_KEY and FOUNDRY_CHANNEL_ID must be set ' +
24
+ 'to push events to Foundry. Plugin will no-op until configured.');
25
+ warnedMissing = true;
26
+ }
27
+ return null;
28
+ }
29
+ return { apiUrl, apiKey, channelId };
30
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Thin client that POSTs Medusa events to Foundry's webhook receiver.
3
+ *
4
+ * URL: ${apiUrl}/api/v1/channels/${channelId}/webhook/medusa
5
+ * Auth: Authorization: Bearer ${apiKey} (Foundry's standard fims_* key)
6
+ * Body: { topic, payload }
7
+ *
8
+ * Errors are logged but never thrown — a Foundry-side blip must not
9
+ * break the merchant's order checkout. Phase 3 adds a retry queue
10
+ * with exponential backoff so transient failures don't drop events.
11
+ */
12
+ declare class FoundryClient {
13
+ postEvent(topic: string, payload: Record<string, unknown>): Promise<void>;
14
+ }
15
+ export declare const foundryClient: FoundryClient;
16
+ export {};
@@ -0,0 +1,43 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.foundryClient = void 0;
4
+ const config_1 = require("./config");
5
+ /**
6
+ * Thin client that POSTs Medusa events to Foundry's webhook receiver.
7
+ *
8
+ * URL: ${apiUrl}/api/v1/channels/${channelId}/webhook/medusa
9
+ * Auth: Authorization: Bearer ${apiKey} (Foundry's standard fims_* key)
10
+ * Body: { topic, payload }
11
+ *
12
+ * Errors are logged but never thrown — a Foundry-side blip must not
13
+ * break the merchant's order checkout. Phase 3 adds a retry queue
14
+ * with exponential backoff so transient failures don't drop events.
15
+ */
16
+ class FoundryClient {
17
+ async postEvent(topic, payload) {
18
+ const config = (0, config_1.resolveConfig)();
19
+ if (!config)
20
+ return;
21
+ const url = `${config.apiUrl}/api/v1/channels/${config.channelId}/webhook/medusa`;
22
+ try {
23
+ const response = await fetch(url, {
24
+ method: 'POST',
25
+ headers: {
26
+ Authorization: `Bearer ${config.apiKey}`,
27
+ 'Content-Type': 'application/json',
28
+ Accept: 'application/json',
29
+ },
30
+ body: JSON.stringify({ topic, payload }),
31
+ });
32
+ if (!response.ok) {
33
+ // eslint-disable-next-line no-console
34
+ console.error(`[medusa-plugin-foundry-ims] post failed for ${topic}: ${response.status} ${response.statusText}`);
35
+ }
36
+ }
37
+ catch (err) {
38
+ // eslint-disable-next-line no-console
39
+ console.error(`[medusa-plugin-foundry-ims] post error for ${topic}:`, err);
40
+ }
41
+ }
42
+ }
43
+ exports.foundryClient = new FoundryClient();
@@ -0,0 +1,15 @@
1
+ import type { SubscriberArgs, SubscriberConfig } from '@medusajs/framework';
2
+ /**
3
+ * Fires when a fulfillment gets a tracking number — Medusa-side
4
+ * shipment creation. Foundry's receiver upserts the corresponding
5
+ * Shipment row so tracking is visible in Foundry's admin without
6
+ * waiting for the next polling cycle.
7
+ *
8
+ * Payload includes order_id; Foundry re-fetches the full order and
9
+ * walks its shipments[] array.
10
+ */
11
+ export default function fulfillmentShipmentCreatedHandler({ event: { data }, }: SubscriberArgs<{
12
+ id: string;
13
+ order_id: string;
14
+ }>): Promise<void>;
15
+ export declare const config: SubscriberConfig;
@@ -0,0 +1,23 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.config = void 0;
4
+ exports.default = fulfillmentShipmentCreatedHandler;
5
+ const foundry_client_1 = require("../lib/foundry-client");
6
+ /**
7
+ * Fires when a fulfillment gets a tracking number — Medusa-side
8
+ * shipment creation. Foundry's receiver upserts the corresponding
9
+ * Shipment row so tracking is visible in Foundry's admin without
10
+ * waiting for the next polling cycle.
11
+ *
12
+ * Payload includes order_id; Foundry re-fetches the full order and
13
+ * walks its shipments[] array.
14
+ */
15
+ async function fulfillmentShipmentCreatedHandler({ event: { data }, }) {
16
+ await foundry_client_1.foundryClient.postEvent('fulfillment.shipment_created', {
17
+ orderId: data.order_id,
18
+ fulfillmentId: data.id,
19
+ });
20
+ }
21
+ exports.config = {
22
+ event: 'fulfillment.shipment_created',
23
+ };
@@ -0,0 +1,10 @@
1
+ import type { SubscriberArgs, SubscriberConfig } from '@medusajs/framework';
2
+ /**
3
+ * Fires on order cancellation. Foundry's receiver releases all active
4
+ * reservations on this order — frees up the inventory for other
5
+ * channels.
6
+ */
7
+ export default function orderCanceledHandler({ event: { data }, }: SubscriberArgs<{
8
+ id: string;
9
+ }>): Promise<void>;
10
+ export declare const config: SubscriberConfig;
@@ -0,0 +1,16 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.config = void 0;
4
+ exports.default = orderCanceledHandler;
5
+ const foundry_client_1 = require("../lib/foundry-client");
6
+ /**
7
+ * Fires on order cancellation. Foundry's receiver releases all active
8
+ * reservations on this order — frees up the inventory for other
9
+ * channels.
10
+ */
11
+ async function orderCanceledHandler({ event: { data }, }) {
12
+ await foundry_client_1.foundryClient.postEvent('order.canceled', { orderId: data.id });
13
+ }
14
+ exports.config = {
15
+ event: 'order.canceled',
16
+ };
@@ -0,0 +1,12 @@
1
+ import type { SubscriberArgs, SubscriberConfig } from '@medusajs/framework';
2
+ /**
3
+ * Fires when a customer places an order. We forward a thin envelope
4
+ * `{ topic: 'order.placed', payload: { orderId } }` to Foundry —
5
+ * Foundry's receiver re-fetches the full order via its existing
6
+ * MedusaAdapter so the data flow stays consistent with the cron-poll
7
+ * path.
8
+ */
9
+ export default function orderPlacedHandler({ event: { data }, }: SubscriberArgs<{
10
+ id: string;
11
+ }>): Promise<void>;
12
+ export declare const config: SubscriberConfig;
@@ -0,0 +1,18 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.config = void 0;
4
+ exports.default = orderPlacedHandler;
5
+ const foundry_client_1 = require("../lib/foundry-client");
6
+ /**
7
+ * Fires when a customer places an order. We forward a thin envelope
8
+ * `{ topic: 'order.placed', payload: { orderId } }` to Foundry —
9
+ * Foundry's receiver re-fetches the full order via its existing
10
+ * MedusaAdapter so the data flow stays consistent with the cron-poll
11
+ * path.
12
+ */
13
+ async function orderPlacedHandler({ event: { data }, }) {
14
+ await foundry_client_1.foundryClient.postEvent('order.placed', { orderId: data.id });
15
+ }
16
+ exports.config = {
17
+ event: 'order.placed',
18
+ };
@@ -0,0 +1,10 @@
1
+ import type { SubscriberArgs, SubscriberConfig } from '@medusajs/framework';
2
+ /**
3
+ * Fires on payment + fulfillment status changes. Foundry's receiver
4
+ * re-fetches the order, recomputes status, and transitions any active
5
+ * reservations (commit on shipped/completed, release on refunded).
6
+ */
7
+ export default function orderUpdatedHandler({ event: { data }, }: SubscriberArgs<{
8
+ id: string;
9
+ }>): Promise<void>;
10
+ export declare const config: SubscriberConfig;
@@ -0,0 +1,16 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.config = void 0;
4
+ exports.default = orderUpdatedHandler;
5
+ const foundry_client_1 = require("../lib/foundry-client");
6
+ /**
7
+ * Fires on payment + fulfillment status changes. Foundry's receiver
8
+ * re-fetches the order, recomputes status, and transitions any active
9
+ * reservations (commit on shipped/completed, release on refunded).
10
+ */
11
+ async function orderUpdatedHandler({ event: { data }, }) {
12
+ await foundry_client_1.foundryClient.postEvent('order.updated', { orderId: data.id });
13
+ }
14
+ exports.config = {
15
+ event: 'order.updated',
16
+ };
package/README.md ADDED
@@ -0,0 +1,113 @@
1
+ # medusa-plugin-foundry-ims
2
+
3
+ Real-time integration between [Medusa](https://medusajs.com) and [Foundry IMS](https://foundryims.com). Pushes order, inventory, and fulfillment events from your Medusa store to Foundry the moment they happen — replaces polling with native Medusa event subscribers.
4
+
5
+ ## What this plugin does
6
+
7
+ Subscribes to Medusa events and forwards them to your Foundry workspace:
8
+
9
+ | Medusa event | What Foundry does |
10
+ |---|---|
11
+ | `order.placed` | Imports the order, creates inventory reservations |
12
+ | `order.updated` | Updates order status, transitions reservations on shipment/refund |
13
+ | `order.canceled` | Releases all active reservations |
14
+ | `fulfillment.shipment_created` | Records the tracking number against the order |
15
+
16
+ The plugin sends a thin event envelope (`{ topic, payload: { orderId } }`); Foundry refetches the full order from your Medusa admin API for the rest. Your Foundry API key is the auth — no separate webhook secret to manage.
17
+
18
+ ## Requirements
19
+
20
+ - Medusa **v2.3.0+** (plugin support landed in 2.3.0)
21
+ - Node 20+
22
+ - A Foundry IMS account at [foundryims.com](https://foundryims.com) with:
23
+ - A `MEDUSA` channel created (Sales Channels → Add Channel → Medusa)
24
+ - An API key (Settings → API Keys → Create)
25
+
26
+ ## Install
27
+
28
+ ```bash
29
+ npm install medusa-plugin-foundry-ims
30
+ # or
31
+ yarn add medusa-plugin-foundry-ims
32
+ ```
33
+
34
+ In your `medusa-config.ts`:
35
+
36
+ ```ts
37
+ import { defineConfig } from "@medusajs/framework/utils"
38
+
39
+ module.exports = defineConfig({
40
+ // ...your existing config...
41
+ plugins: [
42
+ {
43
+ resolve: "medusa-plugin-foundry-ims",
44
+ options: {},
45
+ },
46
+ ],
47
+ })
48
+ ```
49
+
50
+ ## Configure
51
+
52
+ The plugin reads three environment variables:
53
+
54
+ | Var | Required | Default | Where to get it |
55
+ |---|---|---|---|
56
+ | `FOUNDRY_API_KEY` | yes | — | Foundry → Settings → API Keys → Create. Starts with `fims_`. |
57
+ | `FOUNDRY_CHANNEL_ID` | yes | — | Foundry → Sales Channels → Medusa → URL contains the channel UUID. |
58
+ | `FOUNDRY_API_URL` | no | `https://api.foundryims.com` | Override only for self-hosted or staging Foundry instances. |
59
+
60
+ Add to your Medusa server's `.env`:
61
+
62
+ ```bash
63
+ FOUNDRY_API_KEY=fims_xxxxxxxxxxxxxxxxxxxxxxxxxxxxx
64
+ FOUNDRY_CHANNEL_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
65
+ ```
66
+
67
+ Restart your Medusa server. Place a test order — Foundry should receive it within ~1 second.
68
+
69
+ ## Verifying the connection
70
+
71
+ After a test order:
72
+
73
+ ```bash
74
+ curl -H "Authorization: Bearer $FOUNDRY_API_KEY" \
75
+ "$FOUNDRY_API_URL/api/v1/orders?limit=5"
76
+ ```
77
+
78
+ You should see your order in the response. If not, check your Medusa logs for `[medusa-plugin-foundry-ims]` warnings — usually a missing env var.
79
+
80
+ ## Troubleshooting
81
+
82
+ **Plugin warns "FOUNDRY_API_KEY and FOUNDRY_CHANNEL_ID must be set..." on startup.**
83
+ The plugin couldn't find one or both required env vars. Double-check your `.env` and that you've restarted Medusa.
84
+
85
+ **Orders aren't appearing in Foundry.**
86
+ 1. Confirm the API key is valid (curl test above).
87
+ 2. Confirm the channel ID matches the one shown in Foundry's URL.
88
+ 3. Check Medusa server logs for `[medusa-plugin-foundry-ims] post failed: ...`.
89
+ 4. Confirm `orderSyncEnabled` is on for the Foundry channel (Foundry → Sales Channels → Medusa → Settings).
90
+
91
+ **The plugin works but Foundry shows the wrong status / out-of-date inventory.**
92
+ Foundry refetches via the Medusa admin API on every event. Make sure the `adminApiToken` configured on the Foundry channel is still valid (Settings → Foundry → Sales Channels → Medusa → Edit credentials).
93
+
94
+ ## What this plugin does NOT do
95
+
96
+ - It does not push *outbound* changes (Foundry → Medusa). That direction is handled by Foundry's existing Medusa adapter.
97
+ - It does not provide an admin UI inside Medusa (yet — coming in v0.2.0).
98
+ - It does not have built-in retry logic for failed posts (yet — coming in v0.3.0). For now, Foundry's 5-minute order-sync cron is the safety net.
99
+
100
+ ## Roadmap
101
+
102
+ - **v0.2.0** — Admin widget inside Medusa Settings: configure API key + channel ID without env vars.
103
+ - **v0.3.0** — Retry queue for failed event posts. Health-check endpoint. Inventory + product subscribers.
104
+
105
+ ## Support
106
+
107
+ - [Foundry docs](https://foundryims.com/docs)
108
+ - [GitHub issues](https://github.com/Epic-Design-Labs/medusa-plugin-foundry-ims/issues)
109
+ - [Foundry support email](mailto:support@foundryims.com)
110
+
111
+ ## License
112
+
113
+ MIT
package/package.json ADDED
@@ -0,0 +1,63 @@
1
+ {
2
+ "name": "medusa-plugin-foundry-ims",
3
+ "version": "0.1.0",
4
+ "description": "Foundry IMS integration for Medusa — push orders, inventory, and fulfillments to Foundry in real time.",
5
+ "main": ".medusa/server/src/index.js",
6
+ "types": ".medusa/server/src/index.d.ts",
7
+ "files": [
8
+ ".medusa/server"
9
+ ],
10
+ "exports": {
11
+ "./package.json": "./package.json",
12
+ "./workflows": "./.medusa/server/src/workflows/index.js",
13
+ "./.medusa/server/src/modules/*": "./.medusa/server/src/modules/*/index.js",
14
+ "./modules/*": "./.medusa/server/src/modules/*/index.js",
15
+ "./providers/*": "./.medusa/server/src/providers/*/index.js",
16
+ "./*": "./.medusa/server/src/*.js",
17
+ "./admin": {
18
+ "import": "./.medusa/server/src/admin/index.mjs",
19
+ "require": "./.medusa/server/src/admin/index.js",
20
+ "default": "./.medusa/server/src/admin/index.js"
21
+ }
22
+ },
23
+ "keywords": [
24
+ "medusa-v2",
25
+ "medusa-plugin-integration",
26
+ "foundry",
27
+ "foundry-ims",
28
+ "inventory",
29
+ "multichannel",
30
+ "fulfillment",
31
+ "order-management"
32
+ ],
33
+ "author": "Foundry IMS",
34
+ "license": "MIT",
35
+ "homepage": "https://foundryims.com",
36
+ "repository": {
37
+ "type": "git",
38
+ "url": "https://github.com/Epic-Design-Labs/medusa-plugin-foundry-ims.git"
39
+ },
40
+ "bugs": {
41
+ "url": "https://github.com/Epic-Design-Labs/medusa-plugin-foundry-ims/issues"
42
+ },
43
+ "scripts": {
44
+ "build": "tsc -p tsconfig.json",
45
+ "prepack": "tsc -p tsconfig.json",
46
+ "dev": "tsc -p tsconfig.json --watch"
47
+ },
48
+ "peerDependencies": {
49
+ "@medusajs/framework": "^2.3.0",
50
+ "@medusajs/medusa": "^2.3.0"
51
+ },
52
+ "devDependencies": {
53
+ "@medusajs/admin-sdk": "^2.3.0",
54
+ "@medusajs/framework": "^2.3.0",
55
+ "@medusajs/medusa": "^2.3.0",
56
+ "@medusajs/test-utils": "^2.3.0",
57
+ "@swc/core": "1.5.7",
58
+ "typescript": "^5.6.2"
59
+ },
60
+ "engines": {
61
+ "node": ">=20"
62
+ }
63
+ }