@open-mercato/core 0.7.1-develop.7154.1.981330c924 → 0.7.1-develop.7171.1.9b31dbba45
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/.turbo/turbo-build.log +1 -1
- package/AGENTS.md +2 -2
- package/dist/modules/auth/commands/users.js +14 -1
- package/dist/modules/auth/commands/users.js.map +2 -2
- package/dist/modules/auth/data/entities.js +2 -2
- package/dist/modules/auth/data/entities.js.map +2 -2
- package/dist/modules/auth/lib/backendChrome.js +26 -3
- package/dist/modules/auth/lib/backendChrome.js.map +2 -2
- package/dist/modules/auth/lib/profile-sections.js +1 -1
- package/dist/modules/auth/lib/profile-sections.js.map +2 -2
- package/dist/modules/auth/migrations/Migration20260907120000_auth.js +19 -0
- package/dist/modules/auth/migrations/Migration20260907120000_auth.js.map +7 -0
- package/dist/modules/communication_channels/backend/profile/communication-channels/page.meta.js +2 -2
- package/dist/modules/communication_channels/backend/profile/communication-channels/page.meta.js.map +1 -1
- package/dist/modules/customer_accounts/api/admin/demo-accounts.js +118 -0
- package/dist/modules/customer_accounts/api/admin/demo-accounts.js.map +7 -0
- package/dist/modules/customer_accounts/api/invitations/accept.js +23 -6
- package/dist/modules/customer_accounts/api/invitations/accept.js.map +2 -2
- package/dist/modules/customer_accounts/backend/customer_accounts/settings/CustomerAccountsSettingsPageClient.js +10 -13
- package/dist/modules/customer_accounts/backend/customer_accounts/settings/CustomerAccountsSettingsPageClient.js.map +2 -2
- package/dist/modules/customer_accounts/backend/customer_accounts/useDemoPortalAccounts.js +28 -0
- package/dist/modules/customer_accounts/backend/customer_accounts/useDemoPortalAccounts.js.map +7 -0
- package/dist/modules/customer_accounts/backend/customer_accounts/users/PortalUsersPageClient.js +6 -1
- package/dist/modules/customer_accounts/backend/customer_accounts/users/PortalUsersPageClient.js.map +2 -2
- package/dist/modules/customer_accounts/data/entities.js +4 -1
- package/dist/modules/customer_accounts/data/entities.js.map +2 -2
- package/dist/modules/customer_accounts/lib/exampleAccounts.js +9 -0
- package/dist/modules/customer_accounts/lib/exampleAccounts.js.map +7 -0
- package/dist/modules/customer_accounts/migrations/Migration20260827160000_customer_accounts.js +16 -0
- package/dist/modules/customer_accounts/migrations/Migration20260827160000_customer_accounts.js.map +7 -0
- package/dist/modules/customer_accounts/services/customerInvitationService.js +53 -6
- package/dist/modules/customer_accounts/services/customerInvitationService.js.map +2 -2
- package/dist/modules/customer_accounts/setup.js +2 -6
- package/dist/modules/customer_accounts/setup.js.map +2 -2
- package/dist/modules/customer_accounts/widgets/injection/account-status/widget.client.js +55 -40
- package/dist/modules/customer_accounts/widgets/injection/account-status/widget.client.js.map +2 -2
- package/dist/modules/customers/commands/people.js +16 -1
- package/dist/modules/customers/commands/people.js.map +2 -2
- package/dist/modules/data_sync/backend/data-sync/runs/[id]/page.js +18 -36
- package/dist/modules/data_sync/backend/data-sync/runs/[id]/page.js.map +2 -2
- package/dist/modules/entities/api/records.js +18 -15
- package/dist/modules/entities/api/records.js.map +3 -3
- package/dist/modules/entities/lib/scoped-field-defs.js +52 -0
- package/dist/modules/entities/lib/scoped-field-defs.js.map +7 -0
- package/dist/modules/entities/lib/validation.js +4 -35
- package/dist/modules/entities/lib/validation.js.map +2 -2
- package/dist/modules/integrations/backend/integrations/[id]/page.js +32 -54
- package/dist/modules/integrations/backend/integrations/[id]/page.js.map +2 -2
- package/dist/modules/integrations/backend/integrations/bundle/[id]/page.js +12 -29
- package/dist/modules/integrations/backend/integrations/bundle/[id]/page.js.map +2 -2
- package/dist/modules/notifications/backend/profile/notification-preferences/page.meta.js +2 -2
- package/dist/modules/notifications/backend/profile/notification-preferences/page.meta.js.map +1 -1
- package/dist/modules/progress/lib/progressServiceImpl.js +14 -3
- package/dist/modules/progress/lib/progressServiceImpl.js.map +2 -2
- package/dist/modules/sales/backend/sales/documents/[id]/page.js +9 -9
- package/dist/modules/sales/backend/sales/documents/[id]/page.js.map +2 -2
- package/dist/modules/sales/components/documents/LineItemDialog.js +50 -10
- package/dist/modules/sales/components/documents/LineItemDialog.js.map +2 -2
- package/dist/modules/sales/components/documents/PaymentsSection.js +8 -3
- package/dist/modules/sales/components/documents/PaymentsSection.js.map +2 -2
- package/dist/modules/sales/components/documents/ReturnEditDialog.js +2 -7
- package/dist/modules/sales/components/documents/ReturnEditDialog.js.map +2 -2
- package/dist/modules/sales/components/documents/ReturnsSection.js +2 -7
- package/dist/modules/sales/components/documents/ReturnsSection.js.map +2 -2
- package/dist/modules/sales/components/documents/ShipmentsSection.js +3 -8
- package/dist/modules/sales/components/documents/ShipmentsSection.js.map +2 -2
- package/dist/modules/wms/commands/sales-order-assignment.js +12 -0
- package/dist/modules/wms/commands/sales-order-assignment.js.map +2 -2
- package/dist/modules/wms/data/enrichers.js +25 -0
- package/dist/modules/wms/data/enrichers.js.map +2 -2
- package/dist/modules/wms/lib/enricherCacheTags.js +9 -0
- package/dist/modules/wms/lib/enricherCacheTags.js.map +7 -0
- package/dist/modules/wms/lib/invalidateInventoryEnricherCache.js +37 -0
- package/dist/modules/wms/lib/invalidateInventoryEnricherCache.js.map +7 -0
- package/dist/modules/wms/lib/inventoryEnricherCacheSubscriber.js +14 -0
- package/dist/modules/wms/lib/inventoryEnricherCacheSubscriber.js.map +7 -0
- package/dist/modules/wms/subscribers/invalidate-enricher-cache-balance.js +12 -0
- package/dist/modules/wms/subscribers/invalidate-enricher-cache-balance.js.map +7 -0
- package/dist/modules/wms/subscribers/invalidate-enricher-cache-inventory.js +12 -0
- package/dist/modules/wms/subscribers/invalidate-enricher-cache-inventory.js.map +7 -0
- package/dist/modules/wms/subscribers/invalidate-enricher-cache-movement.js +12 -0
- package/dist/modules/wms/subscribers/invalidate-enricher-cache-movement.js.map +7 -0
- package/dist/modules/wms/subscribers/invalidate-enricher-cache-profile.js +12 -0
- package/dist/modules/wms/subscribers/invalidate-enricher-cache-profile.js.map +7 -0
- package/dist/modules/wms/subscribers/invalidate-enricher-cache-reservation.js +12 -0
- package/dist/modules/wms/subscribers/invalidate-enricher-cache-reservation.js.map +7 -0
- package/dist/modules/wms/subscribers/invalidate-enricher-cache-sales-line.js +12 -0
- package/dist/modules/wms/subscribers/invalidate-enricher-cache-sales-line.js.map +7 -0
- package/dist/modules/wms/subscribers/invalidate-enricher-cache-sales-order.js +12 -0
- package/dist/modules/wms/subscribers/invalidate-enricher-cache-sales-order.js.map +7 -0
- package/dist/modules/wms/subscribers/invalidate-enricher-cache-variant.js +12 -0
- package/dist/modules/wms/subscribers/invalidate-enricher-cache-variant.js.map +7 -0
- package/dist/modules/wms/subscribers/invalidate-enricher-cache-warehouse.js +12 -0
- package/dist/modules/wms/subscribers/invalidate-enricher-cache-warehouse.js.map +7 -0
- package/package.json +7 -7
- package/src/modules/auth/commands/users.ts +14 -1
- package/src/modules/auth/data/entities.ts +2 -2
- package/src/modules/auth/lib/backendChrome.tsx +46 -2
- package/src/modules/auth/lib/profile-sections.tsx +14 -3
- package/src/modules/auth/migrations/.snapshot-open-mercato.json +4 -2
- package/src/modules/auth/migrations/Migration20260907120000_auth.ts +21 -0
- package/src/modules/communication_channels/backend/profile/communication-channels/page.meta.ts +2 -2
- package/src/modules/customer_accounts/AGENTS.md +3 -1
- package/src/modules/customer_accounts/api/admin/demo-accounts.ts +149 -0
- package/src/modules/customer_accounts/api/invitations/accept.ts +23 -6
- package/src/modules/customer_accounts/backend/customer_accounts/settings/CustomerAccountsSettingsPageClient.tsx +43 -41
- package/src/modules/customer_accounts/backend/customer_accounts/useDemoPortalAccounts.ts +42 -0
- package/src/modules/customer_accounts/backend/customer_accounts/users/PortalUsersPageClient.tsx +10 -3
- package/src/modules/customer_accounts/data/entities.ts +5 -1
- package/src/modules/customer_accounts/i18n/de.json +4 -2
- package/src/modules/customer_accounts/i18n/en.json +4 -2
- package/src/modules/customer_accounts/i18n/es.json +4 -2
- package/src/modules/customer_accounts/i18n/ko.json +4 -2
- package/src/modules/customer_accounts/i18n/pl.json +4 -2
- package/src/modules/customer_accounts/lib/exampleAccounts.ts +18 -0
- package/src/modules/customer_accounts/migrations/.snapshot-open-mercato.json +5 -7
- package/src/modules/customer_accounts/migrations/Migration20260827160000_customer_accounts.ts +23 -0
- package/src/modules/customer_accounts/services/customerInvitationService.ts +89 -5
- package/src/modules/customer_accounts/setup.ts +2 -6
- package/src/modules/customer_accounts/widgets/injection/account-status/widget.client.tsx +74 -41
- package/src/modules/customers/commands/people.ts +15 -0
- package/src/modules/data_sync/backend/data-sync/runs/[id]/page.tsx +19 -44
- package/src/modules/entities/api/records.ts +24 -15
- package/src/modules/entities/lib/scoped-field-defs.ts +76 -0
- package/src/modules/entities/lib/validation.ts +5 -45
- package/src/modules/integrations/backend/integrations/[id]/page.tsx +33 -62
- package/src/modules/integrations/backend/integrations/bundle/[id]/page.tsx +13 -37
- package/src/modules/notifications/backend/profile/notification-preferences/page.meta.ts +2 -2
- package/src/modules/progress/AGENTS.md +1 -1
- package/src/modules/progress/lib/progressServiceImpl.ts +26 -3
- package/src/modules/sales/backend/sales/documents/[id]/page.tsx +19 -25
- package/src/modules/sales/components/documents/LineItemDialog.tsx +53 -9
- package/src/modules/sales/components/documents/PaymentsSection.tsx +8 -8
- package/src/modules/sales/components/documents/ReturnEditDialog.tsx +2 -8
- package/src/modules/sales/components/documents/ReturnsSection.tsx +8 -7
- package/src/modules/sales/components/documents/ShipmentsSection.tsx +9 -8
- package/src/modules/sales/i18n/de.json +2 -0
- package/src/modules/sales/i18n/en.json +2 -0
- package/src/modules/sales/i18n/es.json +2 -0
- package/src/modules/sales/i18n/ko.json +2 -0
- package/src/modules/sales/i18n/pl.json +2 -0
- package/src/modules/wms/commands/sales-order-assignment.ts +20 -0
- package/src/modules/wms/data/enrichers.ts +25 -0
- package/src/modules/wms/lib/enricherCacheTags.ts +29 -0
- package/src/modules/wms/lib/invalidateInventoryEnricherCache.ts +71 -0
- package/src/modules/wms/lib/inventoryEnricherCacheSubscriber.ts +30 -0
- package/src/modules/wms/subscribers/invalidate-enricher-cache-balance.ts +9 -0
- package/src/modules/wms/subscribers/invalidate-enricher-cache-inventory.ts +9 -0
- package/src/modules/wms/subscribers/invalidate-enricher-cache-movement.ts +9 -0
- package/src/modules/wms/subscribers/invalidate-enricher-cache-profile.ts +9 -0
- package/src/modules/wms/subscribers/invalidate-enricher-cache-reservation.ts +9 -0
- package/src/modules/wms/subscribers/invalidate-enricher-cache-sales-line.ts +14 -0
- package/src/modules/wms/subscribers/invalidate-enricher-cache-sales-order.ts +18 -0
- package/src/modules/wms/subscribers/invalidate-enricher-cache-variant.ts +9 -0
- package/src/modules/wms/subscribers/invalidate-enricher-cache-warehouse.ts +9 -0
|
@@ -24,6 +24,7 @@ import { Badge } from '@open-mercato/ui/primitives/badge'
|
|
|
24
24
|
import { Spinner } from '@open-mercato/ui/primitives/spinner'
|
|
25
25
|
import { Input } from '@open-mercato/ui/primitives/input'
|
|
26
26
|
import { EmailInput } from '@open-mercato/ui/primitives/email-input'
|
|
27
|
+
import { formatDisplayDate, formatDisplayDateTime, toDateInputValue, toUtcDateInputValue } from '@open-mercato/ui/primitives/date-format'
|
|
27
28
|
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@open-mercato/ui/primitives/dialog'
|
|
28
29
|
import { ArrowRightLeft, Building2, CreditCard, Mail, Pencil, Plus, Send, Store, Truck, UserRound, Wand2, X } from 'lucide-react'
|
|
29
30
|
import { FormHeader, type ActionItem } from '@open-mercato/ui/backend/forms'
|
|
@@ -3864,17 +3865,16 @@ export default function SalesDocumentDetailPage({
|
|
|
3864
3865
|
label: t('sales.documents.detail.expectedDeliveryAt.label', 'Expected delivery'),
|
|
3865
3866
|
emptyLabel: t('sales.documents.detail.empty', 'Not set'),
|
|
3866
3867
|
placeholder: t('sales.documents.detail.expectedDeliveryAt.placeholder', 'Add expected delivery date'),
|
|
3867
|
-
|
|
3868
|
-
|
|
3869
|
-
|
|
3868
|
+
// UTC, not local: this editor submits a bare `yyyy-MM-dd`, which `z.coerce.date()`
|
|
3869
|
+
// stores as UTC midnight. Reading it back locally names the previous day west of UTC.
|
|
3870
|
+
value: toUtcDateInputValue(record?.expectedDeliveryAt),
|
|
3870
3871
|
onSave: handleUpdateExpectedDeliveryAt,
|
|
3871
3872
|
inputType: 'date',
|
|
3872
3873
|
renderDisplay: (params) => {
|
|
3873
3874
|
const { value, emptyLabel } = params
|
|
3874
|
-
|
|
3875
|
-
|
|
3876
|
-
|
|
3877
|
-
return <span className="text-sm text-muted-foreground">{emptyLabel}</span>
|
|
3875
|
+
return (
|
|
3876
|
+
<span className="text-sm text-muted-foreground">{formatDisplayDate(value, locale) ?? emptyLabel}</span>
|
|
3877
|
+
)
|
|
3878
3878
|
},
|
|
3879
3879
|
})
|
|
3880
3880
|
}
|
|
@@ -3915,10 +3915,10 @@ export default function SalesDocumentDetailPage({
|
|
|
3915
3915
|
render: () => (
|
|
3916
3916
|
<SectionCard title={t('sales.documents.detail.timestamps', 'Timestamps')} muted>
|
|
3917
3917
|
<p className="text-sm text-muted-foreground">
|
|
3918
|
-
{t('sales.documents.detail.created', 'Created')}: {record?.createdAt ?? '—'}
|
|
3918
|
+
{t('sales.documents.detail.created', 'Created')}: {formatDisplayDateTime(record?.createdAt, locale) ?? '—'}
|
|
3919
3919
|
</p>
|
|
3920
3920
|
<p className="text-sm text-muted-foreground">
|
|
3921
|
-
{t('sales.documents.detail.updated', 'Updated')}: {record?.updatedAt ?? '—'}
|
|
3921
|
+
{t('sales.documents.detail.updated', 'Updated')}: {formatDisplayDateTime(record?.updatedAt, locale) ?? '—'}
|
|
3922
3922
|
</p>
|
|
3923
3923
|
</SectionCard>
|
|
3924
3924
|
),
|
|
@@ -4707,28 +4707,22 @@ export default function SalesDocumentDetailPage({
|
|
|
4707
4707
|
<InlineTextEditor
|
|
4708
4708
|
key="date"
|
|
4709
4709
|
label={t('sales.documents.detail.date', 'Date')}
|
|
4710
|
-
|
|
4711
|
-
|
|
4712
|
-
|
|
4713
|
-
|
|
4714
|
-
|
|
4715
|
-
: null
|
|
4716
|
-
}
|
|
4710
|
+
// Two readings on purpose. `placedAt` is settable from this date input, so it is
|
|
4711
|
+
// stored as UTC midnight and must be read back in UTC or the field flips a day west
|
|
4712
|
+
// of UTC. `createdAt` is only ever stamped from a clock — a real instant, whose local
|
|
4713
|
+
// day is the one to show.
|
|
4714
|
+
value={toUtcDateInputValue(record?.placedAt) ?? toDateInputValue(record?.createdAt)}
|
|
4717
4715
|
emptyLabel={t('sales.documents.detail.empty', 'Not set')}
|
|
4718
4716
|
onSave={handleUpdatePlacedAt}
|
|
4719
4717
|
inputType="date"
|
|
4720
4718
|
activateOnClick
|
|
4721
4719
|
containerClassName="h-full"
|
|
4722
4720
|
saveLabel={t('customers.people.detail.inline.saveShortcut')}
|
|
4723
|
-
renderDisplay={({ value, emptyLabel }) =>
|
|
4724
|
-
|
|
4725
|
-
|
|
4726
|
-
|
|
4727
|
-
|
|
4728
|
-
) : (
|
|
4729
|
-
<span className="text-sm text-muted-foreground">{emptyLabel}</span>
|
|
4730
|
-
)
|
|
4731
|
-
}
|
|
4721
|
+
renderDisplay={({ value, emptyLabel }) => (
|
|
4722
|
+
<span className="text-sm text-muted-foreground">
|
|
4723
|
+
{formatDisplayDate(value, locale) ?? emptyLabel}
|
|
4724
|
+
</span>
|
|
4725
|
+
)}
|
|
4732
4726
|
/>
|
|
4733
4727
|
</div>
|
|
4734
4728
|
|
|
@@ -56,6 +56,7 @@ import {
|
|
|
56
56
|
extractCustomFieldValues,
|
|
57
57
|
} from "./customFieldHelpers";
|
|
58
58
|
import { canonicalizeUnitCode } from "@open-mercato/shared/lib/units/unitCodes";
|
|
59
|
+
import { parseLocaleNumber } from "@open-mercato/shared/lib/number";
|
|
59
60
|
import { createLogger } from '@open-mercato/shared/lib/logger'
|
|
60
61
|
|
|
61
62
|
const logger = createLogger('sales')
|
|
@@ -490,6 +491,30 @@ export function LineItemDialog({
|
|
|
490
491
|
}: SalesLineDialogProps) {
|
|
491
492
|
const t = useT();
|
|
492
493
|
const locale = useLocale();
|
|
494
|
+
// These fields are hand-rolled text inputs, so the raw string a user typed reaches the
|
|
495
|
+
// submit handler. It carries the separator the surrounding UI displays, which follows the
|
|
496
|
+
// application locale — `110,70` under Polish (issue #5552). A blank field keeps its old
|
|
497
|
+
// "must be greater than 0" message; only genuinely unparseable input returns null.
|
|
498
|
+
const parseUserNumber = React.useCallback(
|
|
499
|
+
(value: unknown): number | null => {
|
|
500
|
+
if (value == null) return 0;
|
|
501
|
+
if (typeof value === "number") return Number.isFinite(value) ? value : null;
|
|
502
|
+
if (typeof value !== "string") return null;
|
|
503
|
+
if (!value.trim()) return 0;
|
|
504
|
+
return parseLocaleNumber(value, locale);
|
|
505
|
+
},
|
|
506
|
+
[locale],
|
|
507
|
+
);
|
|
508
|
+
const numberExample = React.useMemo(() => {
|
|
509
|
+
try {
|
|
510
|
+
return new Intl.NumberFormat(locale, {
|
|
511
|
+
minimumFractionDigits: 2,
|
|
512
|
+
maximumFractionDigits: 2,
|
|
513
|
+
}).format(110.7);
|
|
514
|
+
} catch {
|
|
515
|
+
return "110.70";
|
|
516
|
+
}
|
|
517
|
+
}, [locale]);
|
|
493
518
|
const scope = useOrganizationScopeDetail();
|
|
494
519
|
const resolvedOrganizationId = organizationId ?? scope.organizationId ?? null;
|
|
495
520
|
const resolvedTenantId = tenantId ?? scope.tenantId ?? null;
|
|
@@ -952,7 +977,7 @@ export function LineItemDialog({
|
|
|
952
977
|
try {
|
|
953
978
|
const params = new URLSearchParams({ productId, pageSize: "20" });
|
|
954
979
|
if (variantId) params.set("variantId", variantId);
|
|
955
|
-
const quantityValue =
|
|
980
|
+
const quantityValue = parseUserNumber(quantity) ?? Number.NaN;
|
|
956
981
|
if (Number.isFinite(quantityValue) && quantityValue > 0) {
|
|
957
982
|
params.set("quantity", String(quantityValue));
|
|
958
983
|
}
|
|
@@ -1093,7 +1118,7 @@ export function LineItemDialog({
|
|
|
1093
1118
|
setPriceLoading(false);
|
|
1094
1119
|
}
|
|
1095
1120
|
},
|
|
1096
|
-
[t],
|
|
1121
|
+
[parseUserNumber, t],
|
|
1097
1122
|
);
|
|
1098
1123
|
|
|
1099
1124
|
const selectPriceAfterRefresh = React.useCallback(
|
|
@@ -1136,7 +1161,7 @@ export function LineItemDialog({
|
|
|
1136
1161
|
fromUnit: string | null | undefined,
|
|
1137
1162
|
toUnit: string | null | undefined,
|
|
1138
1163
|
): string | null => {
|
|
1139
|
-
const amount =
|
|
1164
|
+
const amount = parseUserNumber(rawUnitPrice) ?? Number.NaN;
|
|
1140
1165
|
if (!Number.isFinite(amount) || amount <= 0) return null;
|
|
1141
1166
|
const fromCode = normalizeUnitCode(fromUnit);
|
|
1142
1167
|
const toCode = normalizeUnitCode(toUnit);
|
|
@@ -1156,7 +1181,7 @@ export function LineItemDialog({
|
|
|
1156
1181
|
if (!Number.isFinite(convertedAmount) || convertedAmount <= 0) return null;
|
|
1157
1182
|
return normalizeUnitPriceInputValue(convertedAmount);
|
|
1158
1183
|
},
|
|
1159
|
-
[resolveUnitPriceFactor],
|
|
1184
|
+
[parseUserNumber, resolveUnitPriceFactor],
|
|
1160
1185
|
);
|
|
1161
1186
|
|
|
1162
1187
|
const applyPriceSelection = React.useCallback(
|
|
@@ -1342,8 +1367,16 @@ export function LineItemDialog({
|
|
|
1342
1367
|
);
|
|
1343
1368
|
}
|
|
1344
1369
|
|
|
1345
|
-
const qtyNumber =
|
|
1346
|
-
if (
|
|
1370
|
+
const qtyNumber = parseUserNumber(values.quantity);
|
|
1371
|
+
if (qtyNumber === null) {
|
|
1372
|
+
const message = t(
|
|
1373
|
+
"sales.documents.items.errorQuantityInvalid",
|
|
1374
|
+
"Enter the quantity as a number, for example {{example}}.",
|
|
1375
|
+
{ example: numberExample },
|
|
1376
|
+
);
|
|
1377
|
+
throw createCrudFormError(message, { quantity: message });
|
|
1378
|
+
}
|
|
1379
|
+
if (qtyNumber <= 0) {
|
|
1347
1380
|
throw createCrudFormError(
|
|
1348
1381
|
t(
|
|
1349
1382
|
"sales.documents.items.errorQuantity",
|
|
@@ -1380,8 +1413,16 @@ export function LineItemDialog({
|
|
|
1380
1413
|
);
|
|
1381
1414
|
})();
|
|
1382
1415
|
|
|
1383
|
-
const unitPriceNumber =
|
|
1384
|
-
if (
|
|
1416
|
+
const unitPriceNumber = parseUserNumber(values.unitPrice);
|
|
1417
|
+
if (unitPriceNumber === null) {
|
|
1418
|
+
const message = t(
|
|
1419
|
+
"sales.documents.items.errorUnitPriceInvalid",
|
|
1420
|
+
"Enter the unit price as a number, for example {{example}}.",
|
|
1421
|
+
{ example: numberExample },
|
|
1422
|
+
);
|
|
1423
|
+
throw createCrudFormError(message, { unitPrice: message });
|
|
1424
|
+
}
|
|
1425
|
+
if (unitPriceNumber <= 0) {
|
|
1385
1426
|
throw createCrudFormError(
|
|
1386
1427
|
t(
|
|
1387
1428
|
"sales.documents.items.errorUnitPrice",
|
|
@@ -1610,6 +1651,8 @@ export function LineItemDialog({
|
|
|
1610
1651
|
variantOption,
|
|
1611
1652
|
onSaved,
|
|
1612
1653
|
closeDialog,
|
|
1654
|
+
numberExample,
|
|
1655
|
+
parseUserNumber,
|
|
1613
1656
|
resolvedOrganizationId,
|
|
1614
1657
|
resolvedTenantId,
|
|
1615
1658
|
],
|
|
@@ -2541,7 +2584,7 @@ export function LineItemDialog({
|
|
|
2541
2584
|
layout: "full",
|
|
2542
2585
|
component: ({ values }: FieldRenderProps) => {
|
|
2543
2586
|
if (isCustomLine) return null;
|
|
2544
|
-
const quantity =
|
|
2587
|
+
const quantity = parseUserNumber(values?.quantity) ?? Number.NaN;
|
|
2545
2588
|
const enteredUnit = normalizeUnitCode(values?.quantityUnit);
|
|
2546
2589
|
if (!Number.isFinite(quantity) || quantity <= 0 || !enteredUnit) {
|
|
2547
2590
|
return (
|
|
@@ -2651,6 +2694,7 @@ export function LineItemDialog({
|
|
|
2651
2694
|
selectPriceAfterRefresh,
|
|
2652
2695
|
hasTaxMetadata,
|
|
2653
2696
|
isShippedOrderLine,
|
|
2697
|
+
parseUserNumber,
|
|
2654
2698
|
]);
|
|
2655
2699
|
|
|
2656
2700
|
const groups = React.useMemo<CrudFormGroup[]>(() => {
|
|
@@ -11,6 +11,7 @@ import { handleSectionMutationError, readRowUpdatedAt } from './optimisticLock'
|
|
|
11
11
|
import type { SectionAction } from '@open-mercato/ui/backend/detail'
|
|
12
12
|
import { RowActions } from '@open-mercato/ui/backend/RowActions'
|
|
13
13
|
import { Button } from '@open-mercato/ui/primitives/button'
|
|
14
|
+
import { formatDisplayDate, formatDisplayDateTime, toUtcDateInputValue } from '@open-mercato/ui/primitives/date-format'
|
|
14
15
|
import { flash } from '@open-mercato/ui/backend/FlashMessages'
|
|
15
16
|
import { useOrganizationScopeDetail } from '@open-mercato/shared/lib/frontend/useOrganizationScope'
|
|
16
17
|
import { useT, useLocale } from '@open-mercato/shared/lib/i18n/context'
|
|
@@ -293,19 +294,18 @@ export function SalesDocumentPaymentsSection({
|
|
|
293
294
|
{
|
|
294
295
|
accessorKey: 'receivedAt',
|
|
295
296
|
header: t('sales.documents.payments.receivedAt', 'Received'),
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
297
|
+
// `receivedAt` is written from a date input (`PaymentDialog`), coerced with
|
|
298
|
+
// `z.coerce.date()` and stored as UTC midnight, so its day must be read in UTC.
|
|
299
|
+
// Reading it locally names the previous day west of UTC — and disagrees with the
|
|
300
|
+
// Edit dialog, which seeds from `receivedAt.slice(0, 10)`, i.e. the UTC day.
|
|
301
|
+
cell: ({ row }) => formatDisplayDate(toUtcDateInputValue(row.original.receivedAt), locale) ?? '—',
|
|
300
302
|
},
|
|
301
303
|
{
|
|
302
304
|
accessorKey: 'createdAt',
|
|
303
305
|
header: t('sales.documents.payments.createdAt', 'Created'),
|
|
304
|
-
cell: ({ row }) =>
|
|
305
|
-
row.original.createdAt ? new Date(row.original.createdAt).toLocaleString(locale) : '—',
|
|
306
|
+
cell: ({ row }) => formatDisplayDateTime(row.original.createdAt, locale) ?? '—',
|
|
306
307
|
meta: {
|
|
307
|
-
tooltipContent: (row: PaymentRow) =>
|
|
308
|
-
row.createdAt ? new Date(row.createdAt).toLocaleString(locale) : undefined,
|
|
308
|
+
tooltipContent: (row: PaymentRow) => formatDisplayDateTime(row.createdAt, locale) ?? undefined,
|
|
309
309
|
},
|
|
310
310
|
},
|
|
311
311
|
{
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use client"
|
|
2
2
|
|
|
3
3
|
import * as React from 'react'
|
|
4
|
+
import { toUtcDateInputValue } from '@open-mercato/ui/primitives/date-format'
|
|
4
5
|
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@open-mercato/ui/primitives/dialog'
|
|
5
6
|
import { useDialogKeyHandler } from '@open-mercato/ui/hooks/useDialogKeyHandler'
|
|
6
7
|
import { CrudForm, type CrudField } from '@open-mercato/ui/backend/CrudForm'
|
|
@@ -36,13 +37,6 @@ type ReturnEditDialogProps = {
|
|
|
36
37
|
onSaved: () => Promise<void>
|
|
37
38
|
}
|
|
38
39
|
|
|
39
|
-
function toDateInputValue(value: string | null | undefined): string {
|
|
40
|
-
if (!value) return ''
|
|
41
|
-
const date = new Date(value)
|
|
42
|
-
if (Number.isNaN(date.getTime())) return ''
|
|
43
|
-
return date.toISOString().slice(0, 10)
|
|
44
|
-
}
|
|
45
|
-
|
|
46
40
|
export function ReturnEditDialog({
|
|
47
41
|
open,
|
|
48
42
|
returnRecord,
|
|
@@ -62,7 +56,7 @@ export function ReturnEditDialog({
|
|
|
62
56
|
updatedAt: returnRecord?.updatedAt ?? null,
|
|
63
57
|
reason: returnRecord?.reason ?? '',
|
|
64
58
|
notes: returnRecord?.notes ?? '',
|
|
65
|
-
returnedAt:
|
|
59
|
+
returnedAt: toUtcDateInputValue(returnRecord?.returnedAt) ?? '',
|
|
66
60
|
}),
|
|
67
61
|
[returnRecord],
|
|
68
62
|
)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use client"
|
|
2
2
|
|
|
3
3
|
import * as React from 'react'
|
|
4
|
+
import { formatDisplayDate, toUtcDateInputValue } from '@open-mercato/ui/primitives/date-format'
|
|
4
5
|
import { Undo2, Plus } from 'lucide-react'
|
|
5
6
|
import { Button } from '@open-mercato/ui/primitives/button'
|
|
6
7
|
import { Badge } from '@open-mercato/ui/primitives/badge'
|
|
@@ -41,12 +42,8 @@ type SalesReturnsSectionProps = {
|
|
|
41
42
|
documentUpdatedAt?: string | null
|
|
42
43
|
}
|
|
43
44
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
const date = new Date(value)
|
|
47
|
-
if (Number.isNaN(date.getTime())) return null
|
|
48
|
-
return new Intl.DateTimeFormat(locale, { dateStyle: 'medium' }).format(date)
|
|
49
|
-
}
|
|
45
|
+
// One date formatter for this page rather than three with incompatible semantics.
|
|
46
|
+
export { formatDisplayDate }
|
|
50
47
|
|
|
51
48
|
export function SalesReturnsSection({ orderId, currencyCode, documentUpdatedAt }: SalesReturnsSectionProps) {
|
|
52
49
|
const t = useT()
|
|
@@ -329,7 +326,11 @@ export function SalesReturnsSection({ orderId, currencyCode, documentUpdatedAt }
|
|
|
329
326
|
</div>
|
|
330
327
|
</div>
|
|
331
328
|
<div className="whitespace-nowrap text-right text-sm text-muted-foreground">
|
|
332
|
-
{
|
|
329
|
+
{/* `returnedAt` is submitted as a bare `yyyy-MM-dd` by `ReturnEditDialog`, coerced by
|
|
330
|
+
`z.coerce.date()` and returned via `.toISOString()`, and that dialog seeds itself
|
|
331
|
+
back from the UTC day — unconditionally, however the row was written. Matching the
|
|
332
|
+
dialog is the invariant, so the row cannot contradict itself west of UTC. */}
|
|
333
|
+
{formatDisplayDate(toUtcDateInputValue(ret.returnedAt), locale) ?? t('sales.returns.notSet', 'Not set')}
|
|
333
334
|
</div>
|
|
334
335
|
<div className="whitespace-nowrap text-right text-sm font-medium">
|
|
335
336
|
{formatMoney(ret.total, currencyCode ?? null, locale)}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use client"
|
|
2
2
|
|
|
3
3
|
import * as React from 'react'
|
|
4
|
+
import { formatDisplayDate, toUtcDateInputValue } from '@open-mercato/ui/primitives/date-format'
|
|
4
5
|
import { Pencil, Plus, Trash2, Truck } from 'lucide-react'
|
|
5
6
|
import { Button } from '@open-mercato/ui/primitives/button'
|
|
6
7
|
import { Badge } from '@open-mercato/ui/primitives/badge'
|
|
@@ -41,12 +42,8 @@ type SalesShipmentsSectionProps = {
|
|
|
41
42
|
onAddComment?: (body: string) => Promise<void>
|
|
42
43
|
}
|
|
43
44
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
const date = new Date(value)
|
|
47
|
-
if (Number.isNaN(date.getTime())) return null
|
|
48
|
-
return new Intl.DateTimeFormat(locale, { dateStyle: 'medium' }).format(date)
|
|
49
|
-
}
|
|
45
|
+
// One date formatter for this page rather than three with incompatible semantics.
|
|
46
|
+
export { formatDisplayDate }
|
|
50
47
|
|
|
51
48
|
const formatShipmentAddress = (metadata?: Record<string, unknown> | null): string | null => {
|
|
52
49
|
if (!metadata || typeof metadata !== 'object') return null
|
|
@@ -487,8 +484,12 @@ export function SalesShipmentsSection({
|
|
|
487
484
|
) : (
|
|
488
485
|
<div className="grid gap-3 md:grid-cols-2">
|
|
489
486
|
{shipments.map((shipment) => {
|
|
490
|
-
|
|
491
|
-
|
|
487
|
+
// `shippedAt` / `deliveredAt` are written from date inputs (`ShipmentDialog`), coerced
|
|
488
|
+
// by `z.coerce.date()` and stored as UTC midnight, and the dialog seeds itself back from
|
|
489
|
+
// `.slice(0, 10)` — the UTC day. Reading them locally names the previous day west of UTC
|
|
490
|
+
// and disagrees with that dialog on the same row.
|
|
491
|
+
const shippedAt = formatDisplayDate(toUtcDateInputValue(shipment.shippedAt), locale)
|
|
492
|
+
const deliveredAt = formatDisplayDate(toUtcDateInputValue(shipment.deliveredAt), locale)
|
|
492
493
|
const addressSummary = formatShipmentAddress(shipment.metadata)
|
|
493
494
|
const statusLabel =
|
|
494
495
|
shipment.statusLabel ??
|
|
@@ -1016,10 +1016,12 @@
|
|
|
1016
1016
|
"sales.documents.items.errorProductRequired": "Wähle ein Produkt aus, um fortzufahren.",
|
|
1017
1017
|
"sales.documents.items.errorQuantity": "Die Menge muss größer als 0 sein.",
|
|
1018
1018
|
"sales.documents.items.errorQuantityBelowShipped": "Die Menge kann nicht unter die bereits versandten {{shipped}} gesenkt werden.",
|
|
1019
|
+
"sales.documents.items.errorQuantityInvalid": "Geben Sie die Menge als Zahl ein, zum Beispiel {{example}}.",
|
|
1019
1020
|
"sales.documents.items.errorQuantityShipmentsUnknown": "Die Menge kann erst gesenkt werden, wenn die Sendungen dieses Auftrags geladen wurden. Öffnen Sie den Auftrag erneut, um es noch einmal zu versuchen.",
|
|
1020
1021
|
"sales.documents.items.errorSave": "Position konnte nicht gespeichert werden.",
|
|
1021
1022
|
"sales.documents.items.errorScope": "Organisation und Mandant sind erforderlich.",
|
|
1022
1023
|
"sales.documents.items.errorUnitPrice": "Der Stückpreis muss größer als 0 sein.",
|
|
1024
|
+
"sales.documents.items.errorUnitPriceInvalid": "Geben Sie den Stückpreis als Zahl ein, zum Beispiel {{example}}.",
|
|
1023
1025
|
"sales.documents.items.errorVariantRequired": "Wähle eine Variante aus, um fortzufahren.",
|
|
1024
1026
|
"sales.documents.items.errors.uom.conversionNotFound": "Keine Einheitenumrechnung für dieses Produkt gefunden.",
|
|
1025
1027
|
"sales.documents.items.errors.uom.defaultUnitMissing": "Dem Produkt fehlt eine Standard-Basiseinheit.",
|
|
@@ -1016,10 +1016,12 @@
|
|
|
1016
1016
|
"sales.documents.items.errorProductRequired": "Select a product to continue.",
|
|
1017
1017
|
"sales.documents.items.errorQuantity": "Quantity must be greater than 0.",
|
|
1018
1018
|
"sales.documents.items.errorQuantityBelowShipped": "You cannot lower the quantity below the {{shipped}} already shipped.",
|
|
1019
|
+
"sales.documents.items.errorQuantityInvalid": "Enter the quantity as a number, for example {{example}}.",
|
|
1019
1020
|
"sales.documents.items.errorQuantityShipmentsUnknown": "The quantity cannot be lowered until this order's shipments have been read. Reopen the order to try again.",
|
|
1020
1021
|
"sales.documents.items.errorSave": "Failed to save line.",
|
|
1021
1022
|
"sales.documents.items.errorScope": "Organization and tenant are required.",
|
|
1022
1023
|
"sales.documents.items.errorUnitPrice": "Unit price must be greater than 0.",
|
|
1024
|
+
"sales.documents.items.errorUnitPriceInvalid": "Enter the unit price as a number, for example {{example}}.",
|
|
1023
1025
|
"sales.documents.items.errorVariantRequired": "Select a variant to continue.",
|
|
1024
1026
|
"sales.documents.items.errors.uom.conversionNotFound": "No unit conversion found for this product.",
|
|
1025
1027
|
"sales.documents.items.errors.uom.defaultUnitMissing": "Product is missing a default base unit.",
|
|
@@ -1016,10 +1016,12 @@
|
|
|
1016
1016
|
"sales.documents.items.errorProductRequired": "Selecciona un producto para continuar.",
|
|
1017
1017
|
"sales.documents.items.errorQuantity": "La cantidad debe ser mayor que 0.",
|
|
1018
1018
|
"sales.documents.items.errorQuantityBelowShipped": "No puedes reducir la cantidad por debajo de las {{shipped}} ya enviadas.",
|
|
1019
|
+
"sales.documents.items.errorQuantityInvalid": "Introduce la cantidad como un número, por ejemplo {{example}}.",
|
|
1019
1020
|
"sales.documents.items.errorQuantityShipmentsUnknown": "La cantidad no se puede reducir hasta que se carguen los envíos de este pedido. Vuelve a abrir el pedido para intentarlo de nuevo.",
|
|
1020
1021
|
"sales.documents.items.errorSave": "No se pudo guardar la línea.",
|
|
1021
1022
|
"sales.documents.items.errorScope": "Se requieren organización e inquilino.",
|
|
1022
1023
|
"sales.documents.items.errorUnitPrice": "El precio unitario debe ser mayor que 0.",
|
|
1024
|
+
"sales.documents.items.errorUnitPriceInvalid": "Introduce el precio unitario como un número, por ejemplo {{example}}.",
|
|
1023
1025
|
"sales.documents.items.errorVariantRequired": "Selecciona una variante para continuar.",
|
|
1024
1026
|
"sales.documents.items.errors.uom.conversionNotFound": "No se encontró conversión de unidad para este producto.",
|
|
1025
1027
|
"sales.documents.items.errors.uom.defaultUnitMissing": "El producto no tiene una unidad base predeterminada.",
|
|
@@ -1016,10 +1016,12 @@
|
|
|
1016
1016
|
"sales.documents.items.errorProductRequired": "계속하려면 상품을 선택하세요.",
|
|
1017
1017
|
"sales.documents.items.errorQuantity": "수량은 0보다 커야 합니다.",
|
|
1018
1018
|
"sales.documents.items.errorQuantityBelowShipped": "이미 배송된 {{shipped}} 이하로 수량을 줄일 수 없습니다.",
|
|
1019
|
+
"sales.documents.items.errorQuantityInvalid": "수량을 숫자로 입력하세요. 예: {{example}}.",
|
|
1019
1020
|
"sales.documents.items.errorQuantityShipmentsUnknown": "이 주문의 배송 정보를 불러올 때까지 수량을 줄일 수 없습니다. 주문을 다시 열어 재시도하세요.",
|
|
1020
1021
|
"sales.documents.items.errorSave": "품목을 저장하지 못했습니다.",
|
|
1021
1022
|
"sales.documents.items.errorScope": "조직 및 테넌트 정보가 필요합니다.",
|
|
1022
1023
|
"sales.documents.items.errorUnitPrice": "단가는 0보다 커야 합니다.",
|
|
1024
|
+
"sales.documents.items.errorUnitPriceInvalid": "단가를 숫자로 입력하세요. 예: {{example}}.",
|
|
1023
1025
|
"sales.documents.items.errorVariantRequired": "계속하려면 옵션(변형)을 선택하세요.",
|
|
1024
1026
|
"sales.documents.items.errors.uom.conversionNotFound": "이 상품에 대한 단위 변환 정보를 찾을 수 없습니다.",
|
|
1025
1027
|
"sales.documents.items.errors.uom.defaultUnitMissing": "상품에 기본 단위가 설정되어 있지 않습니다.",
|
|
@@ -1016,10 +1016,12 @@
|
|
|
1016
1016
|
"sales.documents.items.errorProductRequired": "Wybierz produkt, aby kontynuować.",
|
|
1017
1017
|
"sales.documents.items.errorQuantity": "Ilość musi być większa niż 0.",
|
|
1018
1018
|
"sales.documents.items.errorQuantityBelowShipped": "Nie można obniżyć ilości poniżej {{shipped}} już wysłanych.",
|
|
1019
|
+
"sales.documents.items.errorQuantityInvalid": "Podaj ilość jako liczbę, na przykład {{example}}.",
|
|
1019
1020
|
"sales.documents.items.errorQuantityShipmentsUnknown": "Nie można obniżyć ilości, dopóki wysyłki tego zamówienia nie zostaną wczytane. Otwórz zamówienie ponownie, aby spróbować jeszcze raz.",
|
|
1020
1021
|
"sales.documents.items.errorSave": "Nie udało się zapisać pozycji.",
|
|
1021
1022
|
"sales.documents.items.errorScope": "Wymagane są organizacja i najemca.",
|
|
1022
1023
|
"sales.documents.items.errorUnitPrice": "Cena jednostkowa musi być większa niż 0.",
|
|
1024
|
+
"sales.documents.items.errorUnitPriceInvalid": "Podaj cenę jednostkową jako liczbę, na przykład {{example}}.",
|
|
1023
1025
|
"sales.documents.items.errorVariantRequired": "Wybierz wariant, aby kontynuować.",
|
|
1024
1026
|
"sales.documents.items.errors.uom.conversionNotFound": "Nie znaleziono przelicznika jednostek dla tego produktu.",
|
|
1025
1027
|
"sales.documents.items.errors.uom.defaultUnitMissing": "Produkt nie ma zdefiniowanej domyślnej jednostki bazowej.",
|
|
@@ -10,6 +10,7 @@ import { SalesOrderWarehouseAssignment, Warehouse } from '../data/entities'
|
|
|
10
10
|
import type { SalesOrderWarehouseAssignInput, SalesOrderWarehouseUnassignInput } from '../data/validators'
|
|
11
11
|
import { z } from 'zod'
|
|
12
12
|
import { reserveInventoryForConfirmedOrder } from '../lib/salesOrderInventoryAutomation'
|
|
13
|
+
import { invalidateWmsInventoryEnricherCache } from '../lib/invalidateInventoryEnricherCache'
|
|
13
14
|
import { ensureOrganizationScope, ensureTenantScope } from './shared'
|
|
14
15
|
|
|
15
16
|
type AssignmentSnapshot = {
|
|
@@ -30,6 +31,17 @@ type UnassignWarehouseUndoPayload = {
|
|
|
30
31
|
before: AssignmentSnapshot | null
|
|
31
32
|
}
|
|
32
33
|
|
|
34
|
+
// These commands emit no WMS event, so the enricher-cache subscribers never see
|
|
35
|
+
// them. `wms.sales-order-inventory` surfaces the assigned warehouse, so every
|
|
36
|
+
// path here that writes an assignment — including the undo handlers — drops the
|
|
37
|
+
// warehouse tag itself.
|
|
38
|
+
async function invalidateAssignmentEnricherCache(
|
|
39
|
+
ctx: CommandRuntimeContext,
|
|
40
|
+
tenantId: string | null | undefined,
|
|
41
|
+
): Promise<void> {
|
|
42
|
+
await invalidateWmsInventoryEnricherCache(ctx.container, tenantId, 'warehouse')
|
|
43
|
+
}
|
|
44
|
+
|
|
33
45
|
function resolveScope(
|
|
34
46
|
ctx: CommandRuntimeContext,
|
|
35
47
|
fallback?: { tenantId?: string | null; organizationId?: string | null },
|
|
@@ -125,6 +137,7 @@ const assignWarehouseHandler: CommandHandler<
|
|
|
125
137
|
existing.assignedBy = ctx.auth?.sub ?? null
|
|
126
138
|
existing.updatedAt = new Date()
|
|
127
139
|
await em.flush()
|
|
140
|
+
await invalidateAssignmentEnricherCache(ctx, scope.tenantId)
|
|
128
141
|
return { assignmentId: existing.id, warehouseId: warehouse.id }
|
|
129
142
|
}
|
|
130
143
|
|
|
@@ -138,6 +151,7 @@ const assignWarehouseHandler: CommandHandler<
|
|
|
138
151
|
})
|
|
139
152
|
em.persist(assignment)
|
|
140
153
|
await em.flush()
|
|
154
|
+
await invalidateAssignmentEnricherCache(ctx, scope.tenantId)
|
|
141
155
|
return { assignmentId: assignment.id, warehouseId: warehouse.id }
|
|
142
156
|
},
|
|
143
157
|
|
|
@@ -187,6 +201,7 @@ const assignWarehouseHandler: CommandHandler<
|
|
|
187
201
|
if (!before) {
|
|
188
202
|
assignment.deletedAt = new Date()
|
|
189
203
|
await em.flush()
|
|
204
|
+
await invalidateAssignmentEnricherCache(ctx, scope.tenantId)
|
|
190
205
|
return
|
|
191
206
|
}
|
|
192
207
|
|
|
@@ -205,12 +220,14 @@ const assignWarehouseHandler: CommandHandler<
|
|
|
205
220
|
if (!previousWarehouse) {
|
|
206
221
|
assignment.deletedAt = new Date()
|
|
207
222
|
await em.flush()
|
|
223
|
+
await invalidateAssignmentEnricherCache(ctx, scope.tenantId)
|
|
208
224
|
return
|
|
209
225
|
}
|
|
210
226
|
|
|
211
227
|
assignment.warehouse = previousWarehouse
|
|
212
228
|
assignment.notes = before.notes ?? null
|
|
213
229
|
await em.flush()
|
|
230
|
+
await invalidateAssignmentEnricherCache(ctx, scope.tenantId)
|
|
214
231
|
}
|
|
215
232
|
},
|
|
216
233
|
}
|
|
@@ -239,6 +256,7 @@ const unassignWarehouseHandler: CommandHandler<
|
|
|
239
256
|
if (existing) {
|
|
240
257
|
existing.deletedAt = new Date()
|
|
241
258
|
await em.flush()
|
|
259
|
+
await invalidateAssignmentEnricherCache(ctx, scope.tenantId)
|
|
242
260
|
}
|
|
243
261
|
|
|
244
262
|
return { ok: true as const }
|
|
@@ -278,6 +296,7 @@ const unassignWarehouseHandler: CommandHandler<
|
|
|
278
296
|
if (existingDeleted) {
|
|
279
297
|
existingDeleted.deletedAt = null
|
|
280
298
|
await em.flush()
|
|
299
|
+
await invalidateAssignmentEnricherCache(ctx, scope.tenantId)
|
|
281
300
|
return
|
|
282
301
|
}
|
|
283
302
|
|
|
@@ -304,6 +323,7 @@ const unassignWarehouseHandler: CommandHandler<
|
|
|
304
323
|
})
|
|
305
324
|
em.persist(restored)
|
|
306
325
|
await em.flush()
|
|
326
|
+
await invalidateAssignmentEnricherCache(ctx, scope.tenantId)
|
|
307
327
|
},
|
|
308
328
|
}
|
|
309
329
|
|
|
@@ -6,6 +6,11 @@ import type { QueryEngine } from '@open-mercato/shared/lib/query/types'
|
|
|
6
6
|
import { E } from '#generated/entities.ids.generated'
|
|
7
7
|
import { InventoryBalance, InventoryReservation, ProductInventoryProfile, SalesOrderWarehouseAssignment, Warehouse } from './entities'
|
|
8
8
|
import { resolvePrimaryWarehouseId } from '../lib/primaryWarehousePolicy'
|
|
9
|
+
import {
|
|
10
|
+
WMS_ENRICHER_CACHE_TTL_MS,
|
|
11
|
+
WMS_INVENTORY_CACHE_TAG,
|
|
12
|
+
WMS_WAREHOUSE_CACHE_TAG,
|
|
13
|
+
} from '../lib/enricherCacheTags'
|
|
9
14
|
|
|
10
15
|
type SalesOrderRecord = Record<string, unknown> & { id?: string }
|
|
11
16
|
type CatalogProductRecord = Record<string, unknown> & { id?: string }
|
|
@@ -419,6 +424,13 @@ const salesOrderInventoryEnricher: ResponseEnricher<SalesOrderRecord, SalesOrder
|
|
|
419
424
|
priority: 40,
|
|
420
425
|
timeout: 2000,
|
|
421
426
|
fallback: EMPTY_ENRICHMENT,
|
|
427
|
+
// Reads reservations, balances, the primary warehouse and the explicit
|
|
428
|
+
// assignment, so it depends on both the inventory and the warehouse tags.
|
|
429
|
+
cache: {
|
|
430
|
+
strategy: 'read-through',
|
|
431
|
+
ttl: WMS_ENRICHER_CACHE_TTL_MS,
|
|
432
|
+
tags: [WMS_INVENTORY_CACHE_TAG, WMS_WAREHOUSE_CACHE_TAG],
|
|
433
|
+
},
|
|
422
434
|
|
|
423
435
|
async enrichOne(record, context: EnricherScope) {
|
|
424
436
|
return (await this.enrichMany!([record], context))[0]
|
|
@@ -583,6 +595,13 @@ const catalogProductInventoryEnricher: ResponseEnricher<CatalogProductRecord, Ca
|
|
|
583
595
|
priority: 40,
|
|
584
596
|
timeout: 2000,
|
|
585
597
|
fallback: EMPTY_CATALOG_ENRICHMENT,
|
|
598
|
+
// Reads inventory profiles and balances; also reads catalog variants, so the
|
|
599
|
+
// catalog variant events invalidate the inventory tag too.
|
|
600
|
+
cache: {
|
|
601
|
+
strategy: 'read-through',
|
|
602
|
+
ttl: WMS_ENRICHER_CACHE_TTL_MS,
|
|
603
|
+
tags: [WMS_INVENTORY_CACHE_TAG],
|
|
604
|
+
},
|
|
586
605
|
|
|
587
606
|
async enrichOne(record, context: EnricherScope) {
|
|
588
607
|
return (await this.enrichMany!([record], context))[0]
|
|
@@ -659,6 +678,12 @@ const catalogVariantInventoryEnricher: ResponseEnricher<CatalogVariantRecord, Ca
|
|
|
659
678
|
priority: 40,
|
|
660
679
|
timeout: 2000,
|
|
661
680
|
fallback: EMPTY_CATALOG_ENRICHMENT,
|
|
681
|
+
// Reads inventory profiles and balances for the variant only.
|
|
682
|
+
cache: {
|
|
683
|
+
strategy: 'read-through',
|
|
684
|
+
ttl: WMS_ENRICHER_CACHE_TTL_MS,
|
|
685
|
+
tags: [WMS_INVENTORY_CACHE_TAG],
|
|
686
|
+
},
|
|
662
687
|
|
|
663
688
|
async enrichOne(record, context: EnricherScope) {
|
|
664
689
|
return (await this.enrichMany!([record], context))[0]
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cache tags shared by the WMS inventory enrichers and the write surfaces that
|
|
3
|
+
* invalidate them. Read side and write side MUST use these constants so the two
|
|
4
|
+
* cannot drift — a tag typo on either side is silent, and the only symptom is
|
|
5
|
+
* inventory that stays stale until the TTL expires.
|
|
6
|
+
*
|
|
7
|
+
* The enricher runner adds `tenant:`, `organization:` and `enricher:` tags of
|
|
8
|
+
* its own, and the cache service prefixes every tag with the current cache
|
|
9
|
+
* tenant, so these are deliberately coarse collection tags rather than
|
|
10
|
+
* per-warehouse ones: `ResponseEnricher.cache.tags` is a static array read from
|
|
11
|
+
* the enricher definition and cannot vary per call. Collection tags
|
|
12
|
+
* over-invalidate slightly; a per-warehouse scheme that the write side could
|
|
13
|
+
* miss would under-invalidate, which is the failure that actually shows wrong
|
|
14
|
+
* stock to a user.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/** Inventory quantities: balances, reservations, movements and profiles. */
|
|
18
|
+
export const WMS_INVENTORY_CACHE_TAG = 'wms:inventory'
|
|
19
|
+
|
|
20
|
+
/** Warehouse identity and sales-order warehouse assignment. */
|
|
21
|
+
export const WMS_WAREHOUSE_CACHE_TAG = 'wms:warehouse'
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Short by design. The tag invalidations below are the primary freshness
|
|
25
|
+
* mechanism; this TTL is the unconditional backstop for anything they miss —
|
|
26
|
+
* a write that failed to resolve the cache service, a cross-process race, or a
|
|
27
|
+
* write surface added later without wiring invalidation.
|
|
28
|
+
*/
|
|
29
|
+
export const WMS_ENRICHER_CACHE_TTL_MS = 30_000
|