@endora-commerce/mod-inventory 0.100.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.
- package/LICENSE +21 -0
- package/README.md +61 -0
- package/dist/admin/api/warehouses-client.d.ts +22 -0
- package/dist/admin/api/warehouses-client.d.ts.map +1 -0
- package/dist/admin/api/warehouses-client.js +32 -0
- package/dist/admin/api/warehouses-client.js.map +1 -0
- package/dist/admin/index.d.ts +43 -0
- package/dist/admin/index.d.ts.map +1 -0
- package/dist/admin/index.js +190 -0
- package/dist/admin/index.js.map +1 -0
- package/dist/admin/pages/AvailabilityNotificationsPage.d.ts +15 -0
- package/dist/admin/pages/AvailabilityNotificationsPage.d.ts.map +1 -0
- package/dist/admin/pages/AvailabilityNotificationsPage.js +111 -0
- package/dist/admin/pages/AvailabilityNotificationsPage.js.map +1 -0
- package/dist/admin/pages/InventoryPage.d.ts +17 -0
- package/dist/admin/pages/InventoryPage.d.ts.map +1 -0
- package/dist/admin/pages/InventoryPage.js +135 -0
- package/dist/admin/pages/InventoryPage.js.map +1 -0
- package/dist/admin/pages/LowStockPage.d.ts +16 -0
- package/dist/admin/pages/LowStockPage.d.ts.map +1 -0
- package/dist/admin/pages/LowStockPage.js +61 -0
- package/dist/admin/pages/LowStockPage.js.map +1 -0
- package/dist/admin/pages/StockImportWizard.d.ts +17 -0
- package/dist/admin/pages/StockImportWizard.d.ts.map +1 -0
- package/dist/admin/pages/StockImportWizard.js +111 -0
- package/dist/admin/pages/StockImportWizard.js.map +1 -0
- package/dist/admin/pages/WarehouseEditor.d.ts +12 -0
- package/dist/admin/pages/WarehouseEditor.d.ts.map +1 -0
- package/dist/admin/pages/WarehouseEditor.js +194 -0
- package/dist/admin/pages/WarehouseEditor.js.map +1 -0
- package/dist/admin/pages/WarehousesList.d.ts +12 -0
- package/dist/admin/pages/WarehousesList.d.ts.map +1 -0
- package/dist/admin/pages/WarehousesList.js +69 -0
- package/dist/admin/pages/WarehousesList.js.map +1 -0
- package/dist/admin/zones/ChannelWarehouses.d.ts +49 -0
- package/dist/admin/zones/ChannelWarehouses.d.ts.map +1 -0
- package/dist/admin/zones/ChannelWarehouses.js +109 -0
- package/dist/admin/zones/ChannelWarehouses.js.map +1 -0
- package/dist/backend/demo/reset.d.ts +18 -0
- package/dist/backend/demo/reset.d.ts.map +1 -0
- package/dist/backend/demo/reset.js +20 -0
- package/dist/backend/demo/reset.js.map +1 -0
- package/dist/backend/demo/rows.d.ts +38 -0
- package/dist/backend/demo/rows.d.ts.map +1 -0
- package/dist/backend/demo/rows.js +38 -0
- package/dist/backend/demo/rows.js.map +1 -0
- package/dist/backend/demo/seed.d.ts +26 -0
- package/dist/backend/demo/seed.d.ts.map +1 -0
- package/dist/backend/demo/seed.js +65 -0
- package/dist/backend/demo/seed.js.map +1 -0
- package/dist/backend/email-templates/transactional-defaults.d.ts +11 -0
- package/dist/backend/email-templates/transactional-defaults.d.ts.map +1 -0
- package/dist/backend/email-templates/transactional-defaults.js +49 -0
- package/dist/backend/email-templates/transactional-defaults.js.map +1 -0
- package/dist/backend/entities/availability-notification.entity.d.ts +49 -0
- package/dist/backend/entities/availability-notification.entity.d.ts.map +1 -0
- package/dist/backend/entities/availability-notification.entity.js +93 -0
- package/dist/backend/entities/availability-notification.entity.js.map +1 -0
- package/dist/backend/entities/inventory-threshold.entity.d.ts +24 -0
- package/dist/backend/entities/inventory-threshold.entity.d.ts.map +1 -0
- package/dist/backend/entities/inventory-threshold.entity.js +61 -0
- package/dist/backend/entities/inventory-threshold.entity.js.map +1 -0
- package/dist/backend/entities/product-warehouse-low-stock-threshold.entity.d.ts +15 -0
- package/dist/backend/entities/product-warehouse-low-stock-threshold.entity.d.ts.map +1 -0
- package/dist/backend/entities/product-warehouse-low-stock-threshold.entity.js +51 -0
- package/dist/backend/entities/product-warehouse-low-stock-threshold.entity.js.map +1 -0
- package/dist/backend/entities/stock-allocation.entity.d.ts +22 -0
- package/dist/backend/entities/stock-allocation.entity.d.ts.map +1 -0
- package/dist/backend/entities/stock-allocation.entity.js +69 -0
- package/dist/backend/entities/stock-allocation.entity.js.map +1 -0
- package/dist/backend/entities/stock-level.entity.d.ts +24 -0
- package/dist/backend/entities/stock-level.entity.d.ts.map +1 -0
- package/dist/backend/entities/stock-level.entity.js +74 -0
- package/dist/backend/entities/stock-level.entity.js.map +1 -0
- package/dist/backend/entities/warehouse-channel-assignment.entity.d.ts +17 -0
- package/dist/backend/entities/warehouse-channel-assignment.entity.d.ts.map +1 -0
- package/dist/backend/entities/warehouse-channel-assignment.entity.js +60 -0
- package/dist/backend/entities/warehouse-channel-assignment.entity.js.map +1 -0
- package/dist/backend/entities/warehouse.entity.d.ts +37 -0
- package/dist/backend/entities/warehouse.entity.d.ts.map +1 -0
- package/dist/backend/entities/warehouse.entity.js +90 -0
- package/dist/backend/entities/warehouse.entity.js.map +1 -0
- package/dist/backend/index.d.ts +139 -0
- package/dist/backend/index.d.ts.map +1 -0
- package/dist/backend/index.js +351 -0
- package/dist/backend/index.js.map +1 -0
- package/dist/backend/plugin.d.ts +113 -0
- package/dist/backend/plugin.d.ts.map +1 -0
- package/dist/backend/plugin.js +75 -0
- package/dist/backend/plugin.js.map +1 -0
- package/dist/backend/prompt-tools.d.ts +23 -0
- package/dist/backend/prompt-tools.d.ts.map +1 -0
- package/dist/backend/prompt-tools.js +77 -0
- package/dist/backend/prompt-tools.js.map +1 -0
- package/dist/backend/routes.admin.d.ts +62 -0
- package/dist/backend/routes.admin.d.ts.map +1 -0
- package/dist/backend/routes.admin.js +383 -0
- package/dist/backend/routes.admin.js.map +1 -0
- package/dist/backend/routes.d.ts +40 -0
- package/dist/backend/routes.d.ts.map +1 -0
- package/dist/backend/routes.js +263 -0
- package/dist/backend/routes.js.map +1 -0
- package/dist/backend/services/audit-references.d.ts +15 -0
- package/dist/backend/services/audit-references.d.ts.map +1 -0
- package/dist/backend/services/audit-references.js +27 -0
- package/dist/backend/services/audit-references.js.map +1 -0
- package/dist/backend/services/availability-notification-service.d.ts +155 -0
- package/dist/backend/services/availability-notification-service.d.ts.map +1 -0
- package/dist/backend/services/availability-notification-service.js +363 -0
- package/dist/backend/services/availability-notification-service.js.map +1 -0
- package/dist/backend/services/availability-worker.d.ts +57 -0
- package/dist/backend/services/availability-worker.d.ts.map +1 -0
- package/dist/backend/services/availability-worker.js +135 -0
- package/dist/backend/services/availability-worker.js.map +1 -0
- package/dist/backend/services/csv-stock-importer.d.ts +52 -0
- package/dist/backend/services/csv-stock-importer.d.ts.map +1 -0
- package/dist/backend/services/csv-stock-importer.js +154 -0
- package/dist/backend/services/csv-stock-importer.js.map +1 -0
- package/dist/backend/services/display-band-resolver.d.ts +19 -0
- package/dist/backend/services/display-band-resolver.d.ts.map +1 -0
- package/dist/backend/services/display-band-resolver.js +21 -0
- package/dist/backend/services/display-band-resolver.js.map +1 -0
- package/dist/backend/services/effective-fulfilment-strategy.d.ts +16 -0
- package/dist/backend/services/effective-fulfilment-strategy.d.ts.map +1 -0
- package/dist/backend/services/effective-fulfilment-strategy.js +16 -0
- package/dist/backend/services/effective-fulfilment-strategy.js.map +1 -0
- package/dist/backend/services/fulfilment-strategy-resolver.d.ts +13 -0
- package/dist/backend/services/fulfilment-strategy-resolver.d.ts.map +1 -0
- package/dist/backend/services/fulfilment-strategy-resolver.js +13 -0
- package/dist/backend/services/fulfilment-strategy-resolver.js.map +1 -0
- package/dist/backend/services/inventory-read-port.d.ts +65 -0
- package/dist/backend/services/inventory-read-port.d.ts.map +1 -0
- package/dist/backend/services/inventory-read-port.js +168 -0
- package/dist/backend/services/inventory-read-port.js.map +1 -0
- package/dist/backend/services/inventory-reservation-apply-port.d.ts +62 -0
- package/dist/backend/services/inventory-reservation-apply-port.d.ts.map +1 -0
- package/dist/backend/services/inventory-reservation-apply-port.js +134 -0
- package/dist/backend/services/inventory-reservation-apply-port.js.map +1 -0
- package/dist/backend/services/low-stock-alert-service.d.ts +87 -0
- package/dist/backend/services/low-stock-alert-service.d.ts.map +1 -0
- package/dist/backend/services/low-stock-alert-service.js +164 -0
- package/dist/backend/services/low-stock-alert-service.js.map +1 -0
- package/dist/backend/services/product-threshold-write.service.d.ts +38 -0
- package/dist/backend/services/product-threshold-write.service.d.ts.map +1 -0
- package/dist/backend/services/product-threshold-write.service.js +80 -0
- package/dist/backend/services/product-threshold-write.service.js.map +1 -0
- package/dist/backend/services/stock-import.service.d.ts +43 -0
- package/dist/backend/services/stock-import.service.d.ts.map +1 -0
- package/dist/backend/services/stock-import.service.js +124 -0
- package/dist/backend/services/stock-import.service.js.map +1 -0
- package/dist/backend/services/stock-level-service.d.ts +146 -0
- package/dist/backend/services/stock-level-service.d.ts.map +1 -0
- package/dist/backend/services/stock-level-service.js +489 -0
- package/dist/backend/services/stock-level-service.js.map +1 -0
- package/dist/backend/services/threshold-admin-service.d.ts +73 -0
- package/dist/backend/services/threshold-admin-service.d.ts.map +1 -0
- package/dist/backend/services/threshold-admin-service.js +167 -0
- package/dist/backend/services/threshold-admin-service.js.map +1 -0
- package/dist/backend/services/threshold-resolver.d.ts +23 -0
- package/dist/backend/services/threshold-resolver.d.ts.map +1 -0
- package/dist/backend/services/threshold-resolver.js +29 -0
- package/dist/backend/services/threshold-resolver.js.map +1 -0
- package/dist/backend/services/threshold-settings-mirror.d.ts +55 -0
- package/dist/backend/services/threshold-settings-mirror.d.ts.map +1 -0
- package/dist/backend/services/threshold-settings-mirror.js +84 -0
- package/dist/backend/services/threshold-settings-mirror.js.map +1 -0
- package/dist/backend/services/warehouse-channel-reconciler.d.ts +19 -0
- package/dist/backend/services/warehouse-channel-reconciler.d.ts.map +1 -0
- package/dist/backend/services/warehouse-channel-reconciler.js +65 -0
- package/dist/backend/services/warehouse-channel-reconciler.js.map +1 -0
- package/dist/backend/services/warehouse-channel-service.d.ts +57 -0
- package/dist/backend/services/warehouse-channel-service.d.ts.map +1 -0
- package/dist/backend/services/warehouse-channel-service.js +205 -0
- package/dist/backend/services/warehouse-channel-service.js.map +1 -0
- package/dist/backend/services/warehouse-country-reference.d.ts +14 -0
- package/dist/backend/services/warehouse-country-reference.d.ts.map +1 -0
- package/dist/backend/services/warehouse-country-reference.js +27 -0
- package/dist/backend/services/warehouse-country-reference.js.map +1 -0
- package/dist/backend/services/warehouse-service.d.ts +86 -0
- package/dist/backend/services/warehouse-service.d.ts.map +1 -0
- package/dist/backend/services/warehouse-service.js +267 -0
- package/dist/backend/services/warehouse-service.js.map +1 -0
- package/dist/manifest.d.ts +219 -0
- package/dist/manifest.d.ts.map +1 -0
- package/dist/manifest.js +377 -0
- package/dist/manifest.js.map +1 -0
- package/dist/migrations/20260503T182812_inventory_workflow.d.ts +39 -0
- package/dist/migrations/20260503T182812_inventory_workflow.d.ts.map +1 -0
- package/dist/migrations/20260503T182812_inventory_workflow.js +205 -0
- package/dist/migrations/20260503T182812_inventory_workflow.js.map +1 -0
- package/dist/migrations/20260611T140347_inventory_warehouse_default_low_stock_threshold.d.ts +14 -0
- package/dist/migrations/20260611T140347_inventory_warehouse_default_low_stock_threshold.d.ts.map +1 -0
- package/dist/migrations/20260611T140347_inventory_warehouse_default_low_stock_threshold.js +18 -0
- package/dist/migrations/20260611T140347_inventory_warehouse_default_low_stock_threshold.js.map +1 -0
- package/dist/migrations/20260611T140348_inventory_per_warehouse_low_stock_thresholds.d.ts +28 -0
- package/dist/migrations/20260611T140348_inventory_per_warehouse_low_stock_thresholds.d.ts.map +1 -0
- package/dist/migrations/20260611T140348_inventory_per_warehouse_low_stock_thresholds.js +48 -0
- package/dist/migrations/20260611T140348_inventory_per_warehouse_low_stock_thresholds.js.map +1 -0
- package/dist/migrations/20260818T081243_inventory_stock_allocation_order_item_fk.d.ts +44 -0
- package/dist/migrations/20260818T081243_inventory_stock_allocation_order_item_fk.d.ts.map +1 -0
- package/dist/migrations/20260818T081243_inventory_stock_allocation_order_item_fk.js +81 -0
- package/dist/migrations/20260818T081243_inventory_stock_allocation_order_item_fk.js.map +1 -0
- package/dist/migrations/20260830T182139_inventory_organization_attribution.d.ts +109 -0
- package/dist/migrations/20260830T182139_inventory_organization_attribution.d.ts.map +1 -0
- package/dist/migrations/20260830T182139_inventory_organization_attribution.js +178 -0
- package/dist/migrations/20260830T182139_inventory_organization_attribution.js.map +1 -0
- package/dist/migrations/20260912T125716_inventory_organization_warehouses.d.ts +34 -0
- package/dist/migrations/20260912T125716_inventory_organization_warehouses.d.ts.map +1 -0
- package/dist/migrations/20260912T125716_inventory_organization_warehouses.js +50 -0
- package/dist/migrations/20260912T125716_inventory_organization_warehouses.js.map +1 -0
- package/dist/migrations/index.d.ts +32 -0
- package/dist/migrations/index.d.ts.map +1 -0
- package/dist/migrations/index.js +39 -0
- package/dist/migrations/index.js.map +1 -0
- package/dist/ports/index.d.ts +183 -0
- package/dist/ports/index.d.ts.map +1 -0
- package/dist/ports/index.js +2 -0
- package/dist/ports/index.js.map +1 -0
- package/docs/inventory.md +177 -0
- package/i18n/en.json +32 -0
- package/i18n/pl.json +32 -0
- package/package.json +105 -0
- package/tailwind.css +14 -0
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { availabilityNotificationRequestSchema, inventoryDisplayModeSchema, isProductVisibleTo, } from '@endora-commerce/contracts';
|
|
3
|
+
import { getResolvedChannel } from '@endora-commerce/platform/kernel';
|
|
4
|
+
import { productAudienceOf } from '@endora-commerce/platform/http';
|
|
5
|
+
import { StockLevel } from './entities/stock-level.entity.js';
|
|
6
|
+
import { resolveDisplayBand } from './services/display-band-resolver.js';
|
|
7
|
+
import { resolveThresholds } from './services/threshold-resolver.js';
|
|
8
|
+
import { InventoryThreshold } from './entities/inventory-threshold.entity.js';
|
|
9
|
+
import { INVENTORY_SETTING_CODES } from '../manifest.js';
|
|
10
|
+
export async function registerInventoryRoutes(app, deps) {
|
|
11
|
+
const { emFactory, catalogProducts, catalogCategories, customerAccounts, availabilityService, requireCustomer, resolveCustomerContext, channelResolver, warehouseChannelService, settingsService, } = deps;
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
// Notify-when-available (foundation 001 backward-compat)
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
// Foundation 001 path — customer-only, pre-fills email from the
|
|
16
|
+
// customer account. Kept for backwards compatibility with any
|
|
17
|
+
// existing storefront/admin callers.
|
|
18
|
+
app.post('/api/v1/catalog/products/:id/notify-when-available', {
|
|
19
|
+
preHandler: requireCustomer,
|
|
20
|
+
schema: { body: availabilityNotificationRequestSchema.optional() },
|
|
21
|
+
}, async (request, reply) => {
|
|
22
|
+
const ctx = resolveCustomerContext(request);
|
|
23
|
+
const body = availabilityNotificationRequestSchema.parse(request.body ?? {});
|
|
24
|
+
const customerAccount = await customerAccounts.findById(ctx.customerAccountId);
|
|
25
|
+
if (!customerAccount) {
|
|
26
|
+
reply.status(403);
|
|
27
|
+
return {
|
|
28
|
+
error: { code: 'FORBIDDEN', message: 'Unknown customer account', requestId: request.id },
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
const subscription = await availabilityService.subscribe({
|
|
32
|
+
customerAccountId: ctx.customerAccountId,
|
|
33
|
+
email: customerAccount.email,
|
|
34
|
+
productId: request.params.id,
|
|
35
|
+
variantId: body.variantId ?? null,
|
|
36
|
+
// Issue #227 — off the actor the auth plugin resolved rather than off
|
|
37
|
+
// the customer context above: `productAudienceOf` is the one helper the
|
|
38
|
+
// six modules that serve a product to a buyer share, so an account with
|
|
39
|
+
// no organisation cannot be answered differently here than next door.
|
|
40
|
+
audience: productAudienceOf(request),
|
|
41
|
+
});
|
|
42
|
+
reply.status(202);
|
|
43
|
+
return {
|
|
44
|
+
data: {
|
|
45
|
+
subscriptionId: subscription.id,
|
|
46
|
+
requestedAt: subscription.requestedAt.toISOString(),
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
});
|
|
50
|
+
// Feature 010 / US6 — public path that accepts both anonymous and
|
|
51
|
+
// authenticated callers. Email is supplied by the body; the storefront
|
|
52
|
+
// pre-fills it from the customer account on the client side when a
|
|
53
|
+
// session exists. Threaded auth is intentionally optional so the
|
|
54
|
+
// anonymous Notify-when-available flow does not require a sign-in.
|
|
55
|
+
app.post('/api/v1/storefront/inventory/notify-when-available', {
|
|
56
|
+
schema: {
|
|
57
|
+
body: z.object({
|
|
58
|
+
productId: z.string().uuid(),
|
|
59
|
+
email: z.string().email(),
|
|
60
|
+
variantId: z.string().uuid().nullable().optional(),
|
|
61
|
+
}),
|
|
62
|
+
},
|
|
63
|
+
}, async (request, reply) => {
|
|
64
|
+
const body = request.body;
|
|
65
|
+
// Best-effort customer-account binding: when the request happens
|
|
66
|
+
// to carry a valid customer context, link the row so the admin
|
|
67
|
+
// queue browser shows the customer id alongside the email.
|
|
68
|
+
let customerAccountId = null;
|
|
69
|
+
try {
|
|
70
|
+
const ctx = resolveCustomerContext(request);
|
|
71
|
+
customerAccountId = ctx.customerAccountId;
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
customerAccountId = null;
|
|
75
|
+
}
|
|
76
|
+
const subscription = await availabilityService.subscribe({
|
|
77
|
+
productId: body.productId,
|
|
78
|
+
email: body.email,
|
|
79
|
+
variantId: body.variantId ?? null,
|
|
80
|
+
customerAccountId,
|
|
81
|
+
// Issue #227 — and deliberately **not** derived from the `try` above.
|
|
82
|
+
// That block answers "which account owns the row", and it swallows its
|
|
83
|
+
// failure; an audience derived from it would read as anonymous for a
|
|
84
|
+
// caller the platform did in fact identify, and worse, would silently
|
|
85
|
+
// become whatever a future edit to that `catch` made it.
|
|
86
|
+
// `productAudienceOf` reads the actor and nothing else.
|
|
87
|
+
audience: productAudienceOf(request),
|
|
88
|
+
});
|
|
89
|
+
reply.status(202);
|
|
90
|
+
return {
|
|
91
|
+
data: {
|
|
92
|
+
subscriptionId: subscription.id,
|
|
93
|
+
requestedAt: subscription.requestedAt.toISOString(),
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
});
|
|
97
|
+
// ---------------------------------------------------------------------------
|
|
98
|
+
// Storefront-public display-mode (US5)
|
|
99
|
+
// ---------------------------------------------------------------------------
|
|
100
|
+
app.get('/api/v1/storefront/inventory/display-mode', async (request) => {
|
|
101
|
+
const channelId = await resolveChannelId(request, channelResolver);
|
|
102
|
+
const mode = await readDisplayMode(channelId, settingsService);
|
|
103
|
+
return { data: { displayMode: mode } };
|
|
104
|
+
});
|
|
105
|
+
// ---------------------------------------------------------------------------
|
|
106
|
+
// Storefront-public per-product stock (US3 + US5)
|
|
107
|
+
// ---------------------------------------------------------------------------
|
|
108
|
+
app.get('/api/v1/storefront/inventory/stock/:id', async (request, reply) => {
|
|
109
|
+
const productId = request.params.id;
|
|
110
|
+
const em = emFactory();
|
|
111
|
+
const product = await catalogProducts.findById(productId);
|
|
112
|
+
// Issue #227 — this route is anonymous and answers "how many of this
|
|
113
|
+
// product are there". Both halves of that are a disclosure about a
|
|
114
|
+
// product an operator restricted: that it exists, and how it is selling.
|
|
115
|
+
if (!product || !isProductVisibleTo(product, productAudienceOf(request))) {
|
|
116
|
+
reply.status(404);
|
|
117
|
+
return {
|
|
118
|
+
error: {
|
|
119
|
+
code: 'PRODUCT_NOT_FOUND',
|
|
120
|
+
message: 'Product not found',
|
|
121
|
+
requestId: request.id,
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
const channelId = await resolveChannelId(request, channelResolver);
|
|
126
|
+
let candidateWarehouseIds = warehouseChannelService
|
|
127
|
+
? await warehouseChannelService.resolveCandidateWarehouseIds(channelId)
|
|
128
|
+
: [];
|
|
129
|
+
// Feature 026 US4 — intersect with the caller's Organization allow-
|
|
130
|
+
// list when one is configured. Empty per-org assignment (or no
|
|
131
|
+
// resolver wired) falls through to the platform defaults above.
|
|
132
|
+
if (deps.resolveOrganizationWarehouseAllowList) {
|
|
133
|
+
const allowList = await deps.resolveOrganizationWarehouseAllowList(request);
|
|
134
|
+
if (allowList && allowList.length > 0) {
|
|
135
|
+
const allowSet = new Set(allowList);
|
|
136
|
+
if (candidateWarehouseIds.length === 0) {
|
|
137
|
+
candidateWarehouseIds = [...allowSet];
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
candidateWarehouseIds = candidateWarehouseIds.filter((id) => allowSet.has(id));
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
let cumulativeOnHand = 0;
|
|
145
|
+
if (candidateWarehouseIds.length > 0) {
|
|
146
|
+
// `em.execute`, not `em.getKnex()`: a knex handle takes its own pooled
|
|
147
|
+
// connection, so this sum would answer from outside any transaction a
|
|
148
|
+
// caller holds open while the `em.find` fallback below answers from
|
|
149
|
+
// inside it — one route, two views of `stock_levels` (issue #207).
|
|
150
|
+
const placeholders = candidateWarehouseIds.map(() => '?').join(', ');
|
|
151
|
+
const sumRows = (await em.execute(`select sum(on_hand) as on_hand
|
|
152
|
+
from stock_levels
|
|
153
|
+
where product_id = ? and warehouse_id in (${placeholders})`, [productId, ...candidateWarehouseIds]));
|
|
154
|
+
cumulativeOnHand = Number(sumRows[0]?.on_hand ?? 0);
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
// Fallback when no channel binding is wired yet.
|
|
158
|
+
const rows = await em.find(StockLevel, { productId });
|
|
159
|
+
cumulativeOnHand = rows.reduce((s, r) => s + r.onHand, 0);
|
|
160
|
+
}
|
|
161
|
+
const displayMode = await readDisplayMode(channelId, settingsService);
|
|
162
|
+
const globalThresholds = await loadGlobalThresholds(em);
|
|
163
|
+
const productThresholds = await loadProductThresholdsRow(em, productId);
|
|
164
|
+
// Asked of `catalog`, not joined out of its `product_categories` table:
|
|
165
|
+
// the bridge row is the owner's and the port has answered for it since
|
|
166
|
+
// D-87 (feature 075, the `inventory` shard). The `findByIds` on the next
|
|
167
|
+
// line was already going to the owner for the threshold columns — this is
|
|
168
|
+
// the first half of the same question arriving at the same place.
|
|
169
|
+
const assignments = await catalogCategories.listAssignmentsForProducts([productId]);
|
|
170
|
+
const categoryIds = assignments.map((a) => a.categoryId);
|
|
171
|
+
const categories = await catalogCategories.findByIds(categoryIds);
|
|
172
|
+
const categoryThresholds = categories.map((c) => ({
|
|
173
|
+
high: c.inventoryThresholdHigh,
|
|
174
|
+
medium: c.inventoryThresholdMedium,
|
|
175
|
+
low: c.inventoryThresholdLow,
|
|
176
|
+
}));
|
|
177
|
+
const thresholds = resolveThresholds({
|
|
178
|
+
productThresholds,
|
|
179
|
+
categoryThresholds,
|
|
180
|
+
globalThresholds,
|
|
181
|
+
});
|
|
182
|
+
const displayBand = resolveDisplayBand({
|
|
183
|
+
manageStock: product.manageStock,
|
|
184
|
+
cumulativeOnHand,
|
|
185
|
+
thresholds,
|
|
186
|
+
});
|
|
187
|
+
// Effective backorder = per-product flag AND the global negative-stock
|
|
188
|
+
// gate; this is what order placement enforces, so the storefront must
|
|
189
|
+
// not advertise backorder when the global gate is off.
|
|
190
|
+
const allowNegativeStock = await readAllowNegativeStock(channelId, settingsService);
|
|
191
|
+
const effectiveBackorderEnabled = product.backorderEnabled && allowNegativeStock;
|
|
192
|
+
const isOutOfStock = product.manageStock && cumulativeOnHand <= 0;
|
|
193
|
+
const showNotifyButton = product.manageStock &&
|
|
194
|
+
!effectiveBackorderEnabled &&
|
|
195
|
+
cumulativeOnHand <= 0;
|
|
196
|
+
const payload = {
|
|
197
|
+
productId,
|
|
198
|
+
manageStock: product.manageStock,
|
|
199
|
+
backorderEnabled: effectiveBackorderEnabled,
|
|
200
|
+
displayMode,
|
|
201
|
+
displayBand,
|
|
202
|
+
exactOnHand: displayMode === 'exact' ? cumulativeOnHand : null,
|
|
203
|
+
isOutOfStock,
|
|
204
|
+
showNotifyButton,
|
|
205
|
+
};
|
|
206
|
+
return { data: payload };
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
async function resolveChannelId(request, _resolver) {
|
|
210
|
+
// The canonical sales-channel resolver middleware already performed
|
|
211
|
+
// header/host/system-default resolution and active-channel validation on
|
|
212
|
+
// this request path, so simply read the resolved channel's id.
|
|
213
|
+
return getResolvedChannel(request).id;
|
|
214
|
+
}
|
|
215
|
+
async function readDisplayMode(channelId, settingsService) {
|
|
216
|
+
if (settingsService && channelId) {
|
|
217
|
+
try {
|
|
218
|
+
const value = await settingsService.get(INVENTORY_SETTING_CODES.DISPLAY_MODE, channelId, inventoryDisplayModeSchema);
|
|
219
|
+
return value;
|
|
220
|
+
}
|
|
221
|
+
catch {
|
|
222
|
+
// fall through
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return 'band';
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Global backorder gate (`inventory.allow_negative_stock`) for a channel.
|
|
229
|
+
* Mirrors `readDisplayMode`: per-channel value → global value → manifest
|
|
230
|
+
* default (false). Failures degrade to false so the storefront never
|
|
231
|
+
* promises a backorder the order placement would reject.
|
|
232
|
+
*/
|
|
233
|
+
async function readAllowNegativeStock(channelId, settingsService) {
|
|
234
|
+
if (settingsService && channelId) {
|
|
235
|
+
try {
|
|
236
|
+
const { z } = await import('zod');
|
|
237
|
+
return await settingsService.get(INVENTORY_SETTING_CODES.ALLOW_NEGATIVE_STOCK, channelId, z.boolean());
|
|
238
|
+
}
|
|
239
|
+
catch {
|
|
240
|
+
// fall through
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
return false;
|
|
244
|
+
}
|
|
245
|
+
async function loadGlobalThresholds(em) {
|
|
246
|
+
const row = await em.findOne(InventoryThreshold, { scopeKind: 'global', scopeId: null });
|
|
247
|
+
return {
|
|
248
|
+
high: row?.thresholdHigh ?? 100,
|
|
249
|
+
medium: row?.thresholdMedium ?? 20,
|
|
250
|
+
low: row?.thresholdLow ?? 1,
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
async function loadProductThresholdsRow(em, productId) {
|
|
254
|
+
const row = await em.findOne(InventoryThreshold, { scopeKind: 'product', scopeId: productId });
|
|
255
|
+
if (!row)
|
|
256
|
+
return null;
|
|
257
|
+
return {
|
|
258
|
+
high: row.thresholdHigh ?? null,
|
|
259
|
+
medium: row.thresholdMedium ?? null,
|
|
260
|
+
low: row.thresholdLow ?? null,
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
//# sourceMappingURL=routes.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"routes.js","sourceRoot":"","sources":["../../src/backend/routes.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { AuditReferenceRegistryPort } from '@endora-commerce/contracts';
|
|
2
|
+
import type { EntityManager } from '@mikro-orm/postgresql';
|
|
3
|
+
/**
|
|
4
|
+
* What a `warehouse` audit row is called, and where the admin app shows it
|
|
5
|
+
* (feature 075, D-87 drain).
|
|
6
|
+
*
|
|
7
|
+
* This is the contribution the inversion was chosen for. `inventory` is the one
|
|
8
|
+
* contributor to `auditReferenceRegistry` whose manifest declares no
|
|
9
|
+
* `activation.nonDeactivatable`, so it is the one an operator can switch off —
|
|
10
|
+
* and `audit_logs` is non-deactivatable, so had it declared this module as a
|
|
11
|
+
* dependency to read a warehouse name, the lifecycle would have refused every
|
|
12
|
+
* attempt to switch this one off. A push costs the operator nothing.
|
|
13
|
+
*/
|
|
14
|
+
export declare function registerInventoryAuditReferences(registry: AuditReferenceRegistryPort, emFactory: () => EntityManager): void;
|
|
15
|
+
//# sourceMappingURL=audit-references.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"audit-references.d.ts","sourceRoot":"","sources":["../../../src/backend/services/audit-references.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,4BAA4B,CAAC;AAC7E,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAG3D;;;;;;;;;;GAUG;AACH,wBAAgB,gCAAgC,CAC9C,QAAQ,EAAE,0BAA0B,EACpC,SAAS,EAAE,MAAM,aAAa,GAC7B,IAAI,CAiBN"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { Warehouse } from '../entities/warehouse.entity.js';
|
|
2
|
+
/**
|
|
3
|
+
* What a `warehouse` audit row is called, and where the admin app shows it
|
|
4
|
+
* (feature 075, D-87 drain).
|
|
5
|
+
*
|
|
6
|
+
* This is the contribution the inversion was chosen for. `inventory` is the one
|
|
7
|
+
* contributor to `auditReferenceRegistry` whose manifest declares no
|
|
8
|
+
* `activation.nonDeactivatable`, so it is the one an operator can switch off —
|
|
9
|
+
* and `audit_logs` is non-deactivatable, so had it declared this module as a
|
|
10
|
+
* dependency to read a warehouse name, the lifecycle would have refused every
|
|
11
|
+
* attempt to switch this one off. A push costs the operator nothing.
|
|
12
|
+
*/
|
|
13
|
+
export function registerInventoryAuditReferences(registry, emFactory) {
|
|
14
|
+
registry.register({
|
|
15
|
+
ownerModuleId: 'inventory',
|
|
16
|
+
referenceType: 'warehouse',
|
|
17
|
+
resolve: async (ids) => {
|
|
18
|
+
const rows = await emFactory().find(Warehouse, { id: { $in: [...ids] } }, { fields: ['id', 'name', 'code'] });
|
|
19
|
+
return rows.map((row) => ({
|
|
20
|
+
id: row.id,
|
|
21
|
+
label: row.name.length > 0 ? row.name : row.code,
|
|
22
|
+
url: `/warehouses/${row.id}`,
|
|
23
|
+
}));
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
//# sourceMappingURL=audit-references.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"audit-references.js","sourceRoot":"","sources":["../../../src/backend/services/audit-references.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,SAAS,EAAE,MAAM,iCAAiC,CAAC;AAE5D;;;;;;;;;;GAUG;AACH,MAAM,UAAU,gCAAgC,CAC9C,QAAoC,EACpC,SAA8B;IAE9B,QAAQ,CAAC,QAAQ,CAAC;QAChB,aAAa,EAAE,WAAW;QAC1B,aAAa,EAAE,WAAW;QAC1B,OAAO,EAAE,KAAK,EAAE,GAAG,EAAE,EAAE;YACrB,MAAM,IAAI,GAAG,MAAM,SAAS,EAAE,CAAC,IAAI,CACjC,SAAS,EACT,EAAE,EAAE,EAAE,EAAE,GAAG,EAAE,CAAC,GAAG,GAAG,CAAC,EAAE,EAAE,EACzB,EAAE,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,CACnC,CAAC;YACF,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;gBACxB,EAAE,EAAE,GAAG,CAAC,EAAE;gBACV,KAAK,EAAE,GAAG,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI;gBAChD,GAAG,EAAE,eAAe,GAAG,CAAC,EAAE,EAAE;aAC7B,CAAC,CAAC,CAAC;QACN,CAAC;KACF,CAAC,CAAC;AACL,CAAC"}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import type { EntityManager } from '@mikro-orm/postgresql';
|
|
2
|
+
import { type CatalogProductReadPort, type CustomerAccountReadPort, type EmailMailerPort, type ProductAudience } from '@endora-commerce/contracts';
|
|
3
|
+
import { type SalesChannelMembershipPort } from '@endora-commerce/platform/kernel';
|
|
4
|
+
import { AvailabilityNotification } from '../entities/availability-notification.entity.js';
|
|
5
|
+
import { StockLevel } from '../entities/stock-level.entity.js';
|
|
6
|
+
import type { InventoryTemplateEmailPort } from './low-stock-alert-service.js';
|
|
7
|
+
export interface SubscribeInput {
|
|
8
|
+
productId: string;
|
|
9
|
+
variantId?: string | null;
|
|
10
|
+
email: string;
|
|
11
|
+
customerAccountId?: string | null;
|
|
12
|
+
/**
|
|
13
|
+
* Who is asking (issue #227). Defaulted to the anonymous audience so a
|
|
14
|
+
* composition that has not been taught to resolve its caller refuses a
|
|
15
|
+
* restricted product rather than queueing a subscription for it — the
|
|
16
|
+
* fail-closed end, and the same default `ComparisonService.addProduct` takes
|
|
17
|
+
* for the same reason. Both storefront routes pass the real one.
|
|
18
|
+
*/
|
|
19
|
+
audience?: ProductAudience;
|
|
20
|
+
}
|
|
21
|
+
export interface AdminListFilter {
|
|
22
|
+
productId?: string;
|
|
23
|
+
status?: 'queued' | 'notified' | 'cancelled';
|
|
24
|
+
page?: number;
|
|
25
|
+
pageSize?: number;
|
|
26
|
+
}
|
|
27
|
+
export interface AdminListRow {
|
|
28
|
+
id: string;
|
|
29
|
+
productId: string;
|
|
30
|
+
productName: string;
|
|
31
|
+
productSku: string;
|
|
32
|
+
customerAccountId: string | null;
|
|
33
|
+
email: string;
|
|
34
|
+
status: 'queued' | 'notified' | 'cancelled';
|
|
35
|
+
queuedAt: string;
|
|
36
|
+
notifiedAt: string | null;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* AvailabilityNotificationService — feature 010 / US6 surface.
|
|
40
|
+
*
|
|
41
|
+
* The original foundation 001 service had a single `subscribe(customerId,
|
|
42
|
+
* productId, variantId?)` entry point. Feature 010 extends it for the
|
|
43
|
+
* three customer paths spelled out in spec.md US6:
|
|
44
|
+
*
|
|
45
|
+
* - storefront customer signed-in → email pre-filled from the account
|
|
46
|
+
* - storefront anonymous → caller supplies the email
|
|
47
|
+
* - admin browses + cancels any queued row
|
|
48
|
+
*
|
|
49
|
+
* The restock fan-out lives on `AvailabilityWorker.dispatchForStockIncrease`
|
|
50
|
+
* (already exists from foundation 001); the worker is rewired in US6 to
|
|
51
|
+
* subscribe to `inventory.adjusted.v1` events with `before === 0 &&
|
|
52
|
+
* after > 0` so a stock-bump fans out one email per queued row.
|
|
53
|
+
*/
|
|
54
|
+
export declare class AvailabilityNotificationService {
|
|
55
|
+
#private;
|
|
56
|
+
private readonly emFactory;
|
|
57
|
+
/** `catalogProductReadPort`, owned by `catalog` (feature 075, Phase C). */
|
|
58
|
+
private readonly catalogProducts;
|
|
59
|
+
/**
|
|
60
|
+
* `customerAccountReadPort`, owned by `customer_accounts` (feature 075).
|
|
61
|
+
*
|
|
62
|
+
* Feature 087 / D-187 gives it a second reader: the organisation an owned
|
|
63
|
+
* subscription carries. It was already **required** here before that
|
|
64
|
+
* ruling and stays so — this module has exactly one construction site
|
|
65
|
+
* (`plugin.ts`) and it holds the gated port, so a composition that cannot
|
|
66
|
+
* answer "which organisation owns this account" cannot build the service
|
|
67
|
+
* at all. Nothing had to be argued for; it is recorded because `pwa` had
|
|
68
|
+
* to widen its constructor for the same read and the two modules now sit
|
|
69
|
+
* in the same state.
|
|
70
|
+
*
|
|
71
|
+
* Resolved through `lazyPort`, so a switched-off `customer_accounts`
|
|
72
|
+
* refuses at the call with the 503 `MODULE_DISABLED` envelope rather than
|
|
73
|
+
* at composition time.
|
|
74
|
+
*/
|
|
75
|
+
private readonly customerAccounts;
|
|
76
|
+
/**
|
|
77
|
+
* `salesChannelMembershipPort` — the sanctioned bridge accessor
|
|
78
|
+
* (Constitution XII), through which {@link subscribe} asks whether the
|
|
79
|
+
* request's channel publishes the product at all.
|
|
80
|
+
*
|
|
81
|
+
* Required rather than optional, and placed before the two optional
|
|
82
|
+
* transports so a construction site cannot leave it off by accident: an
|
|
83
|
+
* optional channel gate is a composition that silently answers without one,
|
|
84
|
+
* which is precisely the state this parameter was added to end. It is a
|
|
85
|
+
* kernel registration rather than a module's, so it adds no edge to this
|
|
86
|
+
* module's manifest.
|
|
87
|
+
*/
|
|
88
|
+
private readonly channelMembership;
|
|
89
|
+
private readonly mailer?;
|
|
90
|
+
private readonly templateEmail?;
|
|
91
|
+
constructor(emFactory: () => EntityManager,
|
|
92
|
+
/** `catalogProductReadPort`, owned by `catalog` (feature 075, Phase C). */
|
|
93
|
+
catalogProducts: CatalogProductReadPort,
|
|
94
|
+
/**
|
|
95
|
+
* `customerAccountReadPort`, owned by `customer_accounts` (feature 075).
|
|
96
|
+
*
|
|
97
|
+
* Feature 087 / D-187 gives it a second reader: the organisation an owned
|
|
98
|
+
* subscription carries. It was already **required** here before that
|
|
99
|
+
* ruling and stays so — this module has exactly one construction site
|
|
100
|
+
* (`plugin.ts`) and it holds the gated port, so a composition that cannot
|
|
101
|
+
* answer "which organisation owns this account" cannot build the service
|
|
102
|
+
* at all. Nothing had to be argued for; it is recorded because `pwa` had
|
|
103
|
+
* to widen its constructor for the same read and the two modules now sit
|
|
104
|
+
* in the same state.
|
|
105
|
+
*
|
|
106
|
+
* Resolved through `lazyPort`, so a switched-off `customer_accounts`
|
|
107
|
+
* refuses at the call with the 503 `MODULE_DISABLED` envelope rather than
|
|
108
|
+
* at composition time.
|
|
109
|
+
*/
|
|
110
|
+
customerAccounts: CustomerAccountReadPort,
|
|
111
|
+
/**
|
|
112
|
+
* `salesChannelMembershipPort` — the sanctioned bridge accessor
|
|
113
|
+
* (Constitution XII), through which {@link subscribe} asks whether the
|
|
114
|
+
* request's channel publishes the product at all.
|
|
115
|
+
*
|
|
116
|
+
* Required rather than optional, and placed before the two optional
|
|
117
|
+
* transports so a construction site cannot leave it off by accident: an
|
|
118
|
+
* optional channel gate is a composition that silently answers without one,
|
|
119
|
+
* which is precisely the state this parameter was added to end. It is a
|
|
120
|
+
* kernel registration rather than a module's, so it adds no edge to this
|
|
121
|
+
* module's manifest.
|
|
122
|
+
*/
|
|
123
|
+
channelMembership: SalesChannelMembershipPort, mailer?: EmailMailerPort | undefined, templateEmail?: InventoryTemplateEmailPort | undefined);
|
|
124
|
+
/**
|
|
125
|
+
* Subscribe a customer / anonymous email to a product's restock signal.
|
|
126
|
+
*
|
|
127
|
+
* Refuses with 404 PRODUCT_NOT_FOUND for every product this caller may not
|
|
128
|
+
* acquire — see {@link #mayAcquire}; with 422 PRODUCT_IN_STOCK when
|
|
129
|
+
* cumulative on-hand > 0; with 409 PRODUCT_UNMANAGED_STOCK when the product
|
|
130
|
+
* opted out of stock tracking; and idempotently returns the existing row on a
|
|
131
|
+
* re-subscribe.
|
|
132
|
+
*/
|
|
133
|
+
subscribe(input: SubscribeInput): Promise<AvailabilityNotification>;
|
|
134
|
+
cancel(id: string): Promise<void>;
|
|
135
|
+
listForAdmin(filter?: AdminListFilter): Promise<{
|
|
136
|
+
items: AdminListRow[];
|
|
137
|
+
page: number;
|
|
138
|
+
pageSize: number;
|
|
139
|
+
total: number;
|
|
140
|
+
}>;
|
|
141
|
+
/**
|
|
142
|
+
* Restock fan-out — invoked by AvailabilityWorker when cumulative on-hand
|
|
143
|
+
* crosses from 0 to > 0 for a (product, variant) pair. Emits one mail
|
|
144
|
+
* per queued row and flips status → 'notified'.
|
|
145
|
+
*/
|
|
146
|
+
processRestockedFanOut(input: {
|
|
147
|
+
productId: string;
|
|
148
|
+
variantId?: string | null;
|
|
149
|
+
}): Promise<{
|
|
150
|
+
notified: number;
|
|
151
|
+
}>;
|
|
152
|
+
/** Stand-in helper used to keep the unused entity import alive. */
|
|
153
|
+
static getStockLevelEntity(): typeof StockLevel;
|
|
154
|
+
}
|
|
155
|
+
//# sourceMappingURL=availability-notification-service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"availability-notification-service.d.ts","sourceRoot":"","sources":["../../../src/backend/services/availability-notification-service.ts"],"names":[],"mappings":"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"}
|