@open-mercato/core 0.6.8-develop.6998.1.d2bc46c56c → 0.6.8-develop.7001.1.03608ab252
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 +2 -2
- package/dist/generated/entities/warranty_claim/index.js +105 -0
- package/dist/generated/entities/warranty_claim/index.js.map +7 -0
- package/dist/generated/entities/warranty_claim_event/index.js +25 -0
- package/dist/generated/entities/warranty_claim_event/index.js.map +7 -0
- package/dist/generated/entities/warranty_claim_line/index.js +77 -0
- package/dist/generated/entities/warranty_claim_line/index.js.map +7 -0
- package/dist/generated/entities/warranty_claim_registration/index.js +43 -0
- package/dist/generated/entities/warranty_claim_registration/index.js.map +7 -0
- package/dist/generated/entities/warranty_claim_sequence/index.js +17 -0
- package/dist/generated/entities/warranty_claim_sequence/index.js.map +7 -0
- package/dist/generated/entities/warranty_claim_settings/index.js +43 -0
- package/dist/generated/entities/warranty_claim_settings/index.js.map +7 -0
- package/dist/generated/entities/warranty_claim_sla_signal/index.js +25 -0
- package/dist/generated/entities/warranty_claim_sla_signal/index.js.map +7 -0
- package/dist/generated/entities/warranty_troubleshooting_guide/index.js +25 -0
- package/dist/generated/entities/warranty_troubleshooting_guide/index.js.map +7 -0
- package/dist/generated/entities/warranty_vendor_policy/index.js +31 -0
- package/dist/generated/entities/warranty_vendor_policy/index.js.map +7 -0
- package/dist/generated/entities.ids.generated.js +12 -0
- package/dist/generated/entities.ids.generated.js.map +2 -2
- package/dist/generated/entity-fields-registry.js +200 -0
- package/dist/generated/entity-fields-registry.js.map +2 -2
- package/dist/helpers/integration/api.js +3 -2
- package/dist/helpers/integration/api.js.map +2 -2
- package/dist/helpers/integration/salesFixtures.js +13 -6
- package/dist/helpers/integration/salesFixtures.js.map +2 -2
- package/dist/modules/attachments/di.js +18 -0
- package/dist/modules/attachments/di.js.map +2 -2
- package/dist/modules/attachments/fields/attachment.js +186 -26
- package/dist/modules/attachments/fields/attachment.js.map +2 -2
- package/dist/modules/attachments/lib/scoped-upload-service.js +202 -0
- package/dist/modules/attachments/lib/scoped-upload-service.js.map +7 -0
- package/dist/modules/attachments/lib/target-access-service.js +39 -0
- package/dist/modules/attachments/lib/target-access-service.js.map +7 -0
- package/dist/modules/auth/api/logout.js +37 -10
- package/dist/modules/auth/api/logout.js.map +2 -2
- package/dist/modules/auth/api/reset/confirm.js +7 -0
- package/dist/modules/auth/api/reset/confirm.js.map +2 -2
- package/dist/modules/auth/api/reset.js +7 -0
- package/dist/modules/auth/api/reset.js.map +2 -2
- package/dist/modules/auth/commands/users.js +13 -0
- package/dist/modules/auth/commands/users.js.map +2 -2
- package/dist/modules/auth/events.js +15 -0
- package/dist/modules/auth/events.js.map +2 -2
- package/dist/modules/customers/api/companies/route.js +1 -0
- package/dist/modules/customers/api/companies/route.js.map +2 -2
- package/dist/modules/customers/backend/customers/companies-v2/[id]/page.js +2 -2
- package/dist/modules/customers/backend/customers/companies-v2/[id]/page.js.map +2 -2
- package/dist/modules/customers/backend/customers/people-v2/[id]/page.js +2 -2
- package/dist/modules/customers/backend/customers/people-v2/[id]/page.js.map +2 -2
- package/dist/modules/customers/components/detail/assignableStaff.js +6 -69
- package/dist/modules/customers/components/detail/assignableStaff.js.map +2 -2
- package/dist/modules/customers/lib/assignableStaff.js +76 -0
- package/dist/modules/customers/lib/assignableStaff.js.map +7 -0
- package/dist/modules/data_sync/lib/batch-stream.js +56 -0
- package/dist/modules/data_sync/lib/batch-stream.js.map +7 -0
- package/dist/modules/data_sync/lib/sync-engine.js +165 -103
- package/dist/modules/data_sync/lib/sync-engine.js.map +2 -2
- package/dist/modules/inbox_ops/api/emails/[id]/reprocess/route.js +4 -0
- package/dist/modules/inbox_ops/api/emails/[id]/reprocess/route.js.map +2 -2
- package/dist/modules/inbox_ops/api/extract/route.js +4 -0
- package/dist/modules/inbox_ops/api/extract/route.js.map +2 -2
- package/dist/modules/inbox_ops/api/webhook/inbound.js +4 -0
- package/dist/modules/inbox_ops/api/webhook/inbound.js.map +2 -2
- package/dist/modules/sales/commands/documents.js +8 -8
- package/dist/modules/sales/commands/documents.js.map +2 -2
- package/dist/modules/warranty_claims/acl.js +75 -0
- package/dist/modules/warranty_claims/acl.js.map +7 -0
- package/dist/modules/warranty_claims/ai-agents.js +161 -0
- package/dist/modules/warranty_claims/ai-agents.js.map +7 -0
- package/dist/modules/warranty_claims/ai-tools.js +591 -0
- package/dist/modules/warranty_claims/ai-tools.js.map +7 -0
- package/dist/modules/warranty_claims/api/ai/assess/route.js +304 -0
- package/dist/modules/warranty_claims/api/ai/assess/route.js.map +7 -0
- package/dist/modules/warranty_claims/api/ai/draft-reply/route.js +127 -0
- package/dist/modules/warranty_claims/api/ai/draft-reply/route.js.map +7 -0
- package/dist/modules/warranty_claims/api/ai/suggest/route.js +177 -0
- package/dist/modules/warranty_claims/api/ai/suggest/route.js.map +7 -0
- package/dist/modules/warranty_claims/api/assign/route.js +124 -0
- package/dist/modules/warranty_claims/api/assign/route.js.map +7 -0
- package/dist/modules/warranty_claims/api/assignees/route.js +97 -0
- package/dist/modules/warranty_claims/api/assignees/route.js.map +7 -0
- package/dist/modules/warranty_claims/api/credit-memo/route.js +152 -0
- package/dist/modules/warranty_claims/api/credit-memo/route.js.map +7 -0
- package/dist/modules/warranty_claims/api/entitlement/route.js +145 -0
- package/dist/modules/warranty_claims/api/entitlement/route.js.map +7 -0
- package/dist/modules/warranty_claims/api/events/route.js +199 -0
- package/dist/modules/warranty_claims/api/events/route.js.map +7 -0
- package/dist/modules/warranty_claims/api/external/claims/route.js +446 -0
- package/dist/modules/warranty_claims/api/external/claims/route.js.map +7 -0
- package/dist/modules/warranty_claims/api/lines/route.js +258 -0
- package/dist/modules/warranty_claims/api/lines/route.js.map +7 -0
- package/dist/modules/warranty_claims/api/openapi.js +27 -0
- package/dist/modules/warranty_claims/api/openapi.js.map +7 -0
- package/dist/modules/warranty_claims/api/portal/attachments/route.js +490 -0
- package/dist/modules/warranty_claims/api/portal/attachments/route.js.map +7 -0
- package/dist/modules/warranty_claims/api/portal/claims/[id]/route.js +178 -0
- package/dist/modules/warranty_claims/api/portal/claims/[id]/route.js.map +7 -0
- package/dist/modules/warranty_claims/api/portal/claims/[id]/shared.js +101 -0
- package/dist/modules/warranty_claims/api/portal/claims/[id]/shared.js.map +7 -0
- package/dist/modules/warranty_claims/api/portal/claims/[id]/submit/route.js +78 -0
- package/dist/modules/warranty_claims/api/portal/claims/[id]/submit/route.js.map +7 -0
- package/dist/modules/warranty_claims/api/portal/claims/[id]/withdraw/route.js +81 -0
- package/dist/modules/warranty_claims/api/portal/claims/[id]/withdraw/route.js.map +7 -0
- package/dist/modules/warranty_claims/api/portal/claims/route.js +462 -0
- package/dist/modules/warranty_claims/api/portal/claims/route.js.map +7 -0
- package/dist/modules/warranty_claims/api/portal/events/route.js +228 -0
- package/dist/modules/warranty_claims/api/portal/events/route.js.map +7 -0
- package/dist/modules/warranty_claims/api/portal/options/route.js +91 -0
- package/dist/modules/warranty_claims/api/portal/options/route.js.map +7 -0
- package/dist/modules/warranty_claims/api/portal/orders/lines/route.js +172 -0
- package/dist/modules/warranty_claims/api/portal/orders/lines/route.js.map +7 -0
- package/dist/modules/warranty_claims/api/portal/orders/route.js +151 -0
- package/dist/modules/warranty_claims/api/portal/orders/route.js.map +7 -0
- package/dist/modules/warranty_claims/api/portal/troubleshooting/route.js +121 -0
- package/dist/modules/warranty_claims/api/portal/troubleshooting/route.js.map +7 -0
- package/dist/modules/warranty_claims/api/receiving/route.js +176 -0
- package/dist/modules/warranty_claims/api/receiving/route.js.map +7 -0
- package/dist/modules/warranty_claims/api/registrations/route.js +382 -0
- package/dist/modules/warranty_claims/api/registrations/route.js.map +7 -0
- package/dist/modules/warranty_claims/api/replacement-order/route.js +150 -0
- package/dist/modules/warranty_claims/api/replacement-order/route.js.map +7 -0
- package/dist/modules/warranty_claims/api/return-label/route.js +235 -0
- package/dist/modules/warranty_claims/api/return-label/route.js.map +7 -0
- package/dist/modules/warranty_claims/api/risk/route.js +101 -0
- package/dist/modules/warranty_claims/api/risk/route.js.map +7 -0
- package/dist/modules/warranty_claims/api/route.js +482 -0
- package/dist/modules/warranty_claims/api/route.js.map +7 -0
- package/dist/modules/warranty_claims/api/sales-return/route.js +151 -0
- package/dist/modules/warranty_claims/api/sales-return/route.js.map +7 -0
- package/dist/modules/warranty_claims/api/settings-general/route.js +195 -0
- package/dist/modules/warranty_claims/api/settings-general/route.js.map +7 -0
- package/dist/modules/warranty_claims/api/stats/route.js +199 -0
- package/dist/modules/warranty_claims/api/stats/route.js.map +7 -0
- package/dist/modules/warranty_claims/api/submit/route.js +120 -0
- package/dist/modules/warranty_claims/api/submit/route.js.map +7 -0
- package/dist/modules/warranty_claims/api/transition/route.js +126 -0
- package/dist/modules/warranty_claims/api/transition/route.js.map +7 -0
- package/dist/modules/warranty_claims/api/troubleshooting-guides/route.js +261 -0
- package/dist/modules/warranty_claims/api/troubleshooting-guides/route.js.map +7 -0
- package/dist/modules/warranty_claims/api/vendor-policies/route.js +311 -0
- package/dist/modules/warranty_claims/api/vendor-policies/route.js.map +7 -0
- package/dist/modules/warranty_claims/api/vendor-recovery/route.js +130 -0
- package/dist/modules/warranty_claims/api/vendor-recovery/route.js.map +7 -0
- package/dist/modules/warranty_claims/api/vendor-recovery-suggestions/route.js +153 -0
- package/dist/modules/warranty_claims/api/vendor-recovery-suggestions/route.js.map +7 -0
- package/dist/modules/warranty_claims/backend/components/AiAssessButtons.js +268 -0
- package/dist/modules/warranty_claims/backend/components/AiAssessButtons.js.map +7 -0
- package/dist/modules/warranty_claims/backend/components/ClaimStageProgress.js +54 -0
- package/dist/modules/warranty_claims/backend/components/ClaimStageProgress.js.map +7 -0
- package/dist/modules/warranty_claims/backend/components/ClaimStatusBadge.js +87 -0
- package/dist/modules/warranty_claims/backend/components/ClaimStatusBadge.js.map +7 -0
- package/dist/modules/warranty_claims/backend/components/ClaimsKpiStrip.js +120 -0
- package/dist/modules/warranty_claims/backend/components/ClaimsKpiStrip.js.map +7 -0
- package/dist/modules/warranty_claims/backend/components/EntitlementLookupBadge.js +135 -0
- package/dist/modules/warranty_claims/backend/components/EntitlementLookupBadge.js.map +7 -0
- package/dist/modules/warranty_claims/backend/components/ReceivingPanel.js +218 -0
- package/dist/modules/warranty_claims/backend/components/ReceivingPanel.js.map +7 -0
- package/dist/modules/warranty_claims/backend/components/ReturnLabelPanel.js +231 -0
- package/dist/modules/warranty_claims/backend/components/ReturnLabelPanel.js.map +7 -0
- package/dist/modules/warranty_claims/backend/components/TroubleshootingWalker.js +82 -0
- package/dist/modules/warranty_claims/backend/components/TroubleshootingWalker.js.map +7 -0
- package/dist/modules/warranty_claims/backend/components/VendorRecoverySuggestionsPanel.js +136 -0
- package/dist/modules/warranty_claims/backend/components/VendorRecoverySuggestionsPanel.js.map +7 -0
- package/dist/modules/warranty_claims/backend/components/WarrantyWorkspace.js +41 -0
- package/dist/modules/warranty_claims/backend/components/WarrantyWorkspace.js.map +7 -0
- package/dist/modules/warranty_claims/backend/components/claimReasonOptions.js +45 -0
- package/dist/modules/warranty_claims/backend/components/claimReasonOptions.js.map +7 -0
- package/dist/modules/warranty_claims/backend/components/claimSla.js +92 -0
- package/dist/modules/warranty_claims/backend/components/claimSla.js.map +7 -0
- package/dist/modules/warranty_claims/backend/components/orderLookup.js +81 -0
- package/dist/modules/warranty_claims/backend/components/orderLookup.js.map +7 -0
- package/dist/modules/warranty_claims/backend/components/productLookup.js +317 -0
- package/dist/modules/warranty_claims/backend/components/productLookup.js.map +7 -0
- package/dist/modules/warranty_claims/backend/components/useUserDisplayNames.js +61 -0
- package/dist/modules/warranty_claims/backend/components/useUserDisplayNames.js.map +7 -0
- package/dist/modules/warranty_claims/backend/page.js +1088 -0
- package/dist/modules/warranty_claims/backend/page.js.map +7 -0
- package/dist/modules/warranty_claims/backend/page.meta.js +16 -0
- package/dist/modules/warranty_claims/backend/page.meta.js.map +7 -0
- package/dist/modules/warranty_claims/backend/warranty_claims/[id]/edit/page.js +427 -0
- package/dist/modules/warranty_claims/backend/warranty_claims/[id]/edit/page.js.map +7 -0
- package/dist/modules/warranty_claims/backend/warranty_claims/[id]/edit/page.meta.js +16 -0
- package/dist/modules/warranty_claims/backend/warranty_claims/[id]/edit/page.meta.js.map +7 -0
- package/dist/modules/warranty_claims/backend/warranty_claims/[id]/page.js +2101 -0
- package/dist/modules/warranty_claims/backend/warranty_claims/[id]/page.js.map +7 -0
- package/dist/modules/warranty_claims/backend/warranty_claims/[id]/page.meta.js +16 -0
- package/dist/modules/warranty_claims/backend/warranty_claims/[id]/page.meta.js.map +7 -0
- package/dist/modules/warranty_claims/backend/warranty_claims/create/page.js +1249 -0
- package/dist/modules/warranty_claims/backend/warranty_claims/create/page.js.map +7 -0
- package/dist/modules/warranty_claims/backend/warranty_claims/create/page.meta.js +16 -0
- package/dist/modules/warranty_claims/backend/warranty_claims/create/page.meta.js.map +7 -0
- package/dist/modules/warranty_claims/backend/warranty_claims/registrations/[id]/edit/page.js +110 -0
- package/dist/modules/warranty_claims/backend/warranty_claims/registrations/[id]/edit/page.js.map +7 -0
- package/dist/modules/warranty_claims/backend/warranty_claims/registrations/[id]/edit/page.meta.js +17 -0
- package/dist/modules/warranty_claims/backend/warranty_claims/registrations/[id]/edit/page.meta.js.map +7 -0
- package/dist/modules/warranty_claims/backend/warranty_claims/registrations/create/page.js +49 -0
- package/dist/modules/warranty_claims/backend/warranty_claims/registrations/create/page.js.map +7 -0
- package/dist/modules/warranty_claims/backend/warranty_claims/registrations/create/page.meta.js +17 -0
- package/dist/modules/warranty_claims/backend/warranty_claims/registrations/create/page.meta.js.map +7 -0
- package/dist/modules/warranty_claims/backend/warranty_claims/registrations/page.js +485 -0
- package/dist/modules/warranty_claims/backend/warranty_claims/registrations/page.js.map +7 -0
- package/dist/modules/warranty_claims/backend/warranty_claims/registrations/page.meta.js +19 -0
- package/dist/modules/warranty_claims/backend/warranty_claims/registrations/page.meta.js.map +7 -0
- package/dist/modules/warranty_claims/backend/warranty_claims/registrations/registrationForm.js +253 -0
- package/dist/modules/warranty_claims/backend/warranty_claims/registrations/registrationForm.js.map +7 -0
- package/dist/modules/warranty_claims/backend/warranty_claims/settings/page.js +1565 -0
- package/dist/modules/warranty_claims/backend/warranty_claims/settings/page.js.map +7 -0
- package/dist/modules/warranty_claims/backend/warranty_claims/settings/page.meta.js +19 -0
- package/dist/modules/warranty_claims/backend/warranty_claims/settings/page.meta.js.map +7 -0
- package/dist/modules/warranty_claims/backend/warranty_claims/troubleshooting-guides/TroubleshootingTreeBuilder.js +472 -0
- package/dist/modules/warranty_claims/backend/warranty_claims/troubleshooting-guides/TroubleshootingTreeBuilder.js.map +7 -0
- package/dist/modules/warranty_claims/backend/warranty_claims/troubleshooting-guides/[id]/edit/page.js +115 -0
- package/dist/modules/warranty_claims/backend/warranty_claims/troubleshooting-guides/[id]/edit/page.js.map +7 -0
- package/dist/modules/warranty_claims/backend/warranty_claims/troubleshooting-guides/[id]/edit/page.meta.js +19 -0
- package/dist/modules/warranty_claims/backend/warranty_claims/troubleshooting-guides/[id]/edit/page.meta.js.map +7 -0
- package/dist/modules/warranty_claims/backend/warranty_claims/troubleshooting-guides/create/page.js +54 -0
- package/dist/modules/warranty_claims/backend/warranty_claims/troubleshooting-guides/create/page.js.map +7 -0
- package/dist/modules/warranty_claims/backend/warranty_claims/troubleshooting-guides/create/page.meta.js +19 -0
- package/dist/modules/warranty_claims/backend/warranty_claims/troubleshooting-guides/create/page.meta.js.map +7 -0
- package/dist/modules/warranty_claims/backend/warranty_claims/troubleshooting-guides/page.js +414 -0
- package/dist/modules/warranty_claims/backend/warranty_claims/troubleshooting-guides/page.js.map +7 -0
- package/dist/modules/warranty_claims/backend/warranty_claims/troubleshooting-guides/page.meta.js +21 -0
- package/dist/modules/warranty_claims/backend/warranty_claims/troubleshooting-guides/page.meta.js.map +7 -0
- package/dist/modules/warranty_claims/backend/warranty_claims/troubleshooting-guides/troubleshootingGuideForm.js +171 -0
- package/dist/modules/warranty_claims/backend/warranty_claims/troubleshooting-guides/troubleshootingGuideForm.js.map +7 -0
- package/dist/modules/warranty_claims/backend/warranty_claims/vendor-policies/[id]/edit/page.js +111 -0
- package/dist/modules/warranty_claims/backend/warranty_claims/vendor-policies/[id]/edit/page.js.map +7 -0
- package/dist/modules/warranty_claims/backend/warranty_claims/vendor-policies/[id]/edit/page.meta.js +19 -0
- package/dist/modules/warranty_claims/backend/warranty_claims/vendor-policies/[id]/edit/page.meta.js.map +7 -0
- package/dist/modules/warranty_claims/backend/warranty_claims/vendor-policies/create/page.js +48 -0
- package/dist/modules/warranty_claims/backend/warranty_claims/vendor-policies/create/page.js.map +7 -0
- package/dist/modules/warranty_claims/backend/warranty_claims/vendor-policies/create/page.meta.js +19 -0
- package/dist/modules/warranty_claims/backend/warranty_claims/vendor-policies/create/page.meta.js.map +7 -0
- package/dist/modules/warranty_claims/backend/warranty_claims/vendor-policies/page.js +429 -0
- package/dist/modules/warranty_claims/backend/warranty_claims/vendor-policies/page.js.map +7 -0
- package/dist/modules/warranty_claims/backend/warranty_claims/vendor-policies/page.meta.js +21 -0
- package/dist/modules/warranty_claims/backend/warranty_claims/vendor-policies/page.meta.js.map +7 -0
- package/dist/modules/warranty_claims/backend/warranty_claims/vendor-policies/vendorPolicyForm.js +182 -0
- package/dist/modules/warranty_claims/backend/warranty_claims/vendor-policies/vendorPolicyForm.js.map +7 -0
- package/dist/modules/warranty_claims/ce.js +48 -0
- package/dist/modules/warranty_claims/ce.js.map +7 -0
- package/dist/modules/warranty_claims/commands/claim-lines.js +911 -0
- package/dist/modules/warranty_claims/commands/claim-lines.js.map +7 -0
- package/dist/modules/warranty_claims/commands/claims.js +2577 -0
- package/dist/modules/warranty_claims/commands/claims.js.map +7 -0
- package/dist/modules/warranty_claims/commands/index.js +4 -0
- package/dist/modules/warranty_claims/commands/index.js.map +7 -0
- package/dist/modules/warranty_claims/commands/settings.js +169 -0
- package/dist/modules/warranty_claims/commands/settings.js.map +7 -0
- package/dist/modules/warranty_claims/commands/shared.js +162 -0
- package/dist/modules/warranty_claims/commands/shared.js.map +7 -0
- package/dist/modules/warranty_claims/data/constants.js +35 -0
- package/dist/modules/warranty_claims/data/constants.js.map +7 -0
- package/dist/modules/warranty_claims/data/enrichers.js +97 -0
- package/dist/modules/warranty_claims/data/enrichers.js.map +7 -0
- package/dist/modules/warranty_claims/data/entities.js +731 -0
- package/dist/modules/warranty_claims/data/entities.js.map +7 -0
- package/dist/modules/warranty_claims/data/validators.js +488 -0
- package/dist/modules/warranty_claims/data/validators.js.map +7 -0
- package/dist/modules/warranty_claims/di.js +29 -0
- package/dist/modules/warranty_claims/di.js.map +7 -0
- package/dist/modules/warranty_claims/encryption.js +28 -0
- package/dist/modules/warranty_claims/encryption.js.map +7 -0
- package/dist/modules/warranty_claims/events.js +81 -0
- package/dist/modules/warranty_claims/events.js.map +7 -0
- package/dist/modules/warranty_claims/extension-points.js +19 -0
- package/dist/modules/warranty_claims/extension-points.js.map +7 -0
- package/dist/modules/warranty_claims/frontend/[orgSlug]/portal/claims/[id]/page.js +825 -0
- package/dist/modules/warranty_claims/frontend/[orgSlug]/portal/claims/[id]/page.js.map +7 -0
- package/dist/modules/warranty_claims/frontend/[orgSlug]/portal/claims/[id]/page.meta.js +11 -0
- package/dist/modules/warranty_claims/frontend/[orgSlug]/portal/claims/[id]/page.meta.js.map +7 -0
- package/dist/modules/warranty_claims/frontend/[orgSlug]/portal/claims/new/page.js +1139 -0
- package/dist/modules/warranty_claims/frontend/[orgSlug]/portal/claims/new/page.js.map +7 -0
- package/dist/modules/warranty_claims/frontend/[orgSlug]/portal/claims/new/page.meta.js +11 -0
- package/dist/modules/warranty_claims/frontend/[orgSlug]/portal/claims/new/page.meta.js.map +7 -0
- package/dist/modules/warranty_claims/frontend/[orgSlug]/portal/claims/page.js +253 -0
- package/dist/modules/warranty_claims/frontend/[orgSlug]/portal/claims/page.js.map +7 -0
- package/dist/modules/warranty_claims/frontend/[orgSlug]/portal/claims/page.meta.js +18 -0
- package/dist/modules/warranty_claims/frontend/[orgSlug]/portal/claims/page.meta.js.map +7 -0
- package/dist/modules/warranty_claims/index.js +15 -0
- package/dist/modules/warranty_claims/index.js.map +7 -0
- package/dist/modules/warranty_claims/lib/aiAssist.js +496 -0
- package/dist/modules/warranty_claims/lib/aiAssist.js.map +7 -0
- package/dist/modules/warranty_claims/lib/apiIdsFilter.js +11 -0
- package/dist/modules/warranty_claims/lib/apiIdsFilter.js.map +7 -0
- package/dist/modules/warranty_claims/lib/assigneeNames.js +153 -0
- package/dist/modules/warranty_claims/lib/assigneeNames.js.map +7 -0
- package/dist/modules/warranty_claims/lib/attachmentVisibility.js +9 -0
- package/dist/modules/warranty_claims/lib/attachmentVisibility.js.map +7 -0
- package/dist/modules/warranty_claims/lib/bulkFeedback.js +8 -0
- package/dist/modules/warranty_claims/lib/bulkFeedback.js.map +7 -0
- package/dist/modules/warranty_claims/lib/businessHours.js +250 -0
- package/dist/modules/warranty_claims/lib/businessHours.js.map +7 -0
- package/dist/modules/warranty_claims/lib/businessHoursForm.js +146 -0
- package/dist/modules/warranty_claims/lib/businessHoursForm.js.map +7 -0
- package/dist/modules/warranty_claims/lib/claimTypeConfig.js +39 -0
- package/dist/modules/warranty_claims/lib/claimTypeConfig.js.map +7 -0
- package/dist/modules/warranty_claims/lib/decryptionSafety.js +17 -0
- package/dist/modules/warranty_claims/lib/decryptionSafety.js.map +7 -0
- package/dist/modules/warranty_claims/lib/deskFilters.js +88 -0
- package/dist/modules/warranty_claims/lib/deskFilters.js.map +7 -0
- package/dist/modules/warranty_claims/lib/dictionaries.js +157 -0
- package/dist/modules/warranty_claims/lib/dictionaries.js.map +7 -0
- package/dist/modules/warranty_claims/lib/dictionaryLabels.js +12 -0
- package/dist/modules/warranty_claims/lib/dictionaryLabels.js.map +7 -0
- package/dist/modules/warranty_claims/lib/emailIntake.js +87 -0
- package/dist/modules/warranty_claims/lib/emailIntake.js.map +7 -0
- package/dist/modules/warranty_claims/lib/escalation.js +58 -0
- package/dist/modules/warranty_claims/lib/escalation.js.map +7 -0
- package/dist/modules/warranty_claims/lib/externalIntake.js +233 -0
- package/dist/modules/warranty_claims/lib/externalIntake.js.map +7 -0
- package/dist/modules/warranty_claims/lib/grading.js +21 -0
- package/dist/modules/warranty_claims/lib/grading.js.map +7 -0
- package/dist/modules/warranty_claims/lib/listSegments.js +18 -0
- package/dist/modules/warranty_claims/lib/listSegments.js.map +7 -0
- package/dist/modules/warranty_claims/lib/orderOwnership.js +25 -0
- package/dist/modules/warranty_claims/lib/orderOwnership.js.map +7 -0
- package/dist/modules/warranty_claims/lib/portalAttachmentUpload.js +11 -0
- package/dist/modules/warranty_claims/lib/portalAttachmentUpload.js.map +7 -0
- package/dist/modules/warranty_claims/lib/portalAttachmentValidation.js +108 -0
- package/dist/modules/warranty_claims/lib/portalAttachmentValidation.js.map +7 -0
- package/dist/modules/warranty_claims/lib/portalClaimAccess.js +26 -0
- package/dist/modules/warranty_claims/lib/portalClaimAccess.js.map +7 -0
- package/dist/modules/warranty_claims/lib/quantity.js +19 -0
- package/dist/modules/warranty_claims/lib/quantity.js.map +7 -0
- package/dist/modules/warranty_claims/lib/risk.js +204 -0
- package/dist/modules/warranty_claims/lib/risk.js.map +7 -0
- package/dist/modules/warranty_claims/lib/settings.js +55 -0
- package/dist/modules/warranty_claims/lib/settings.js.map +7 -0
- package/dist/modules/warranty_claims/lib/stateMachine.js +88 -0
- package/dist/modules/warranty_claims/lib/stateMachine.js.map +7 -0
- package/dist/modules/warranty_claims/lib/timelineFilters.js +16 -0
- package/dist/modules/warranty_claims/lib/timelineFilters.js.map +7 -0
- package/dist/modules/warranty_claims/lib/triage.js +241 -0
- package/dist/modules/warranty_claims/lib/triage.js.map +7 -0
- package/dist/modules/warranty_claims/lib/troubleshooting.js +111 -0
- package/dist/modules/warranty_claims/lib/troubleshooting.js.map +7 -0
- package/dist/modules/warranty_claims/lib/tryResolve.js +11 -0
- package/dist/modules/warranty_claims/lib/tryResolve.js.map +7 -0
- package/dist/modules/warranty_claims/lib/vendorPolicyRecovery.js +101 -0
- package/dist/modules/warranty_claims/lib/vendorPolicyRecovery.js.map +7 -0
- package/dist/modules/warranty_claims/lib/warrantyPreview.js +18 -0
- package/dist/modules/warranty_claims/lib/warrantyPreview.js.map +7 -0
- package/dist/modules/warranty_claims/migrations/Migration20260703191537_warranty_claims.js +32 -0
- package/dist/modules/warranty_claims/migrations/Migration20260703191537_warranty_claims.js.map +7 -0
- package/dist/modules/warranty_claims/migrations/Migration20260704115605_warranty_claims.js +16 -0
- package/dist/modules/warranty_claims/migrations/Migration20260704115605_warranty_claims.js.map +7 -0
- package/dist/modules/warranty_claims/migrations/Migration20260704230000_warranty_claims.js +17 -0
- package/dist/modules/warranty_claims/migrations/Migration20260704230000_warranty_claims.js.map +7 -0
- package/dist/modules/warranty_claims/migrations/Migration20260705121601_warranty_claims.js +29 -0
- package/dist/modules/warranty_claims/migrations/Migration20260705121601_warranty_claims.js.map +7 -0
- package/dist/modules/warranty_claims/migrations/Migration20260709120000_warranty_claims.js +25 -0
- package/dist/modules/warranty_claims/migrations/Migration20260709120000_warranty_claims.js.map +7 -0
- package/dist/modules/warranty_claims/migrations/Migration20260710023607_warranty_claims.js +13 -0
- package/dist/modules/warranty_claims/migrations/Migration20260710023607_warranty_claims.js.map +7 -0
- package/dist/modules/warranty_claims/migrations/Migration20260716203936_warranty_claims.js +15 -0
- package/dist/modules/warranty_claims/migrations/Migration20260716203936_warranty_claims.js.map +7 -0
- package/dist/modules/warranty_claims/migrations/Migration20260717150000_warranty_claims.js +13 -0
- package/dist/modules/warranty_claims/migrations/Migration20260717150000_warranty_claims.js.map +7 -0
- package/dist/modules/warranty_claims/migrations/Migration20260719120000_warranty_claims.js +13 -0
- package/dist/modules/warranty_claims/migrations/Migration20260719120000_warranty_claims.js.map +7 -0
- package/dist/modules/warranty_claims/migrations/Migration20260813103053_warranty_claims.js +12 -0
- package/dist/modules/warranty_claims/migrations/Migration20260813103053_warranty_claims.js.map +7 -0
- package/dist/modules/warranty_claims/notifications.client.js +67 -0
- package/dist/modules/warranty_claims/notifications.client.js.map +7 -0
- package/dist/modules/warranty_claims/notifications.js +103 -0
- package/dist/modules/warranty_claims/notifications.js.map +7 -0
- package/dist/modules/warranty_claims/search.js +174 -0
- package/dist/modules/warranty_claims/search.js.map +7 -0
- package/dist/modules/warranty_claims/services/adjudicationEvaluator.js +202 -0
- package/dist/modules/warranty_claims/services/adjudicationEvaluator.js.map +7 -0
- package/dist/modules/warranty_claims/services/claimNumberGenerator.js +58 -0
- package/dist/modules/warranty_claims/services/claimNumberGenerator.js.map +7 -0
- package/dist/modules/warranty_claims/services/entitlementResolver.js +69 -0
- package/dist/modules/warranty_claims/services/entitlementResolver.js.map +7 -0
- package/dist/modules/warranty_claims/services/returnLabelProvider.js +14 -0
- package/dist/modules/warranty_claims/services/returnLabelProvider.js.map +7 -0
- package/dist/modules/warranty_claims/setup.js +121 -0
- package/dist/modules/warranty_claims/setup.js.map +7 -0
- package/dist/modules/warranty_claims/subscribers/auto-vendor-recovery.js +119 -0
- package/dist/modules/warranty_claims/subscribers/auto-vendor-recovery.js.map +7 -0
- package/dist/modules/warranty_claims/subscribers/claim-assigned-notification.js +47 -0
- package/dist/modules/warranty_claims/subscribers/claim-assigned-notification.js.map +7 -0
- package/dist/modules/warranty_claims/subscribers/claim-customer-reply-notification.js +60 -0
- package/dist/modules/warranty_claims/subscribers/claim-customer-reply-notification.js.map +7 -0
- package/dist/modules/warranty_claims/subscribers/claim-status-notification.js +83 -0
- package/dist/modules/warranty_claims/subscribers/claim-status-notification.js.map +7 -0
- package/dist/modules/warranty_claims/subscribers/claim-submitted-notification.js +46 -0
- package/dist/modules/warranty_claims/subscribers/claim-submitted-notification.js.map +7 -0
- package/dist/modules/warranty_claims/subscribers/email-to-claim.js +135 -0
- package/dist/modules/warranty_claims/subscribers/email-to-claim.js.map +7 -0
- package/dist/modules/warranty_claims/subscribers/return-shipment-tracking.js +164 -0
- package/dist/modules/warranty_claims/subscribers/return-shipment-tracking.js.map +7 -0
- package/dist/modules/warranty_claims/subscribers/vendor-recovery-reconciliation-undo.js +24 -0
- package/dist/modules/warranty_claims/subscribers/vendor-recovery-reconciliation-undo.js.map +7 -0
- package/dist/modules/warranty_claims/subscribers/vendor-recovery-reconciliation.js +26 -0
- package/dist/modules/warranty_claims/subscribers/vendor-recovery-reconciliation.js.map +7 -0
- package/dist/modules/warranty_claims/widgets/dashboard/claims-queue/config.js +15 -0
- package/dist/modules/warranty_claims/widgets/dashboard/claims-queue/config.js.map +7 -0
- package/dist/modules/warranty_claims/widgets/dashboard/claims-queue/widget.client.js +197 -0
- package/dist/modules/warranty_claims/widgets/dashboard/claims-queue/widget.client.js.map +7 -0
- package/dist/modules/warranty_claims/widgets/dashboard/claims-queue/widget.js +28 -0
- package/dist/modules/warranty_claims/widgets/dashboard/claims-queue/widget.js.map +7 -0
- package/dist/modules/warranty_claims/widgets/injection/claims-ai-trigger/widget.client.js +418 -0
- package/dist/modules/warranty_claims/widgets/injection/claims-ai-trigger/widget.client.js.map +7 -0
- package/dist/modules/warranty_claims/widgets/injection/claims-ai-trigger/widget.js +18 -0
- package/dist/modules/warranty_claims/widgets/injection/claims-ai-trigger/widget.js.map +7 -0
- package/dist/modules/warranty_claims/widgets/injection/customer-claims-tab/widget.js +17 -0
- package/dist/modules/warranty_claims/widgets/injection/customer-claims-tab/widget.js.map +7 -0
- package/dist/modules/warranty_claims/widgets/injection/customer-claims-tab.js +146 -0
- package/dist/modules/warranty_claims/widgets/injection/customer-claims-tab.js.map +7 -0
- package/dist/modules/warranty_claims/widgets/injection/order-claims-tab/widget.js +17 -0
- package/dist/modules/warranty_claims/widgets/injection/order-claims-tab/widget.js.map +7 -0
- package/dist/modules/warranty_claims/widgets/injection/order-claims-tab.js +147 -0
- package/dist/modules/warranty_claims/widgets/injection/order-claims-tab.js.map +7 -0
- package/dist/modules/warranty_claims/widgets/injection-table.js +38 -0
- package/dist/modules/warranty_claims/widgets/injection-table.js.map +7 -0
- package/dist/modules/warranty_claims/widgets/notifications/WarrantyClaimNotificationRenderer.js +223 -0
- package/dist/modules/warranty_claims/widgets/notifications/WarrantyClaimNotificationRenderer.js.map +7 -0
- package/dist/modules/warranty_claims/widgets/notifications/index.js +5 -0
- package/dist/modules/warranty_claims/widgets/notifications/index.js.map +7 -0
- package/dist/modules/warranty_claims/workers/sla-escalation-sweep.js +334 -0
- package/dist/modules/warranty_claims/workers/sla-escalation-sweep.js.map +7 -0
- package/generated/entities/warranty_claim/index.ts +51 -0
- package/generated/entities/warranty_claim_event/index.ts +11 -0
- package/generated/entities/warranty_claim_line/index.ts +37 -0
- package/generated/entities/warranty_claim_registration/index.ts +20 -0
- package/generated/entities/warranty_claim_sequence/index.ts +7 -0
- package/generated/entities/warranty_claim_settings/index.ts +20 -0
- package/generated/entities/warranty_claim_sla_signal/index.ts +11 -0
- package/generated/entities/warranty_troubleshooting_guide/index.ts +11 -0
- package/generated/entities/warranty_vendor_policy/index.ts +14 -0
- package/generated/entities.ids.generated.ts +12 -0
- package/generated/entity-fields-registry.ts +200 -0
- package/package.json +7 -7
- package/src/helpers/integration/api.ts +10 -3
- package/src/helpers/integration/salesFixtures.ts +17 -7
- package/src/modules/attachments/di.ts +30 -1
- package/src/modules/attachments/fields/attachment.tsx +215 -23
- package/src/modules/attachments/i18n/de.json +5 -0
- package/src/modules/attachments/i18n/en.json +5 -0
- package/src/modules/attachments/i18n/es.json +5 -0
- package/src/modules/attachments/i18n/ko.json +5 -0
- package/src/modules/attachments/i18n/pl.json +5 -0
- package/src/modules/attachments/lib/scoped-upload-service.ts +257 -0
- package/src/modules/attachments/lib/target-access-service.ts +57 -0
- package/src/modules/auth/api/logout.ts +46 -10
- package/src/modules/auth/api/reset/confirm.ts +7 -0
- package/src/modules/auth/api/reset.ts +7 -0
- package/src/modules/auth/commands/users.ts +22 -0
- package/src/modules/auth/events.ts +15 -0
- package/src/modules/customers/api/companies/route.ts +1 -0
- package/src/modules/customers/backend/customers/companies-v2/[id]/page.tsx +4 -2
- package/src/modules/customers/backend/customers/people-v2/[id]/page.tsx +4 -2
- package/src/modules/customers/components/detail/assignableStaff.ts +14 -177
- package/src/modules/customers/lib/assignableStaff.ts +177 -0
- package/src/modules/data_sync/AGENTS.md +3 -0
- package/src/modules/data_sync/lib/batch-stream.ts +117 -0
- package/src/modules/data_sync/lib/sync-engine.ts +190 -115
- package/src/modules/inbox_ops/api/emails/[id]/reprocess/route.ts +4 -0
- package/src/modules/inbox_ops/api/extract/route.ts +4 -0
- package/src/modules/inbox_ops/api/webhook/inbound.ts +4 -0
- package/src/modules/sales/commands/documents.ts +16 -8
- package/src/modules/warranty_claims/AGENTS.md +49 -0
- package/src/modules/warranty_claims/acl.ts +71 -0
- package/src/modules/warranty_claims/ai-agents.ts +189 -0
- package/src/modules/warranty_claims/ai-tools.ts +711 -0
- package/src/modules/warranty_claims/api/ai/assess/route.ts +371 -0
- package/src/modules/warranty_claims/api/ai/draft-reply/route.ts +149 -0
- package/src/modules/warranty_claims/api/ai/suggest/route.ts +190 -0
- package/src/modules/warranty_claims/api/assign/route.ts +142 -0
- package/src/modules/warranty_claims/api/assignees/route.ts +102 -0
- package/src/modules/warranty_claims/api/credit-memo/route.ts +191 -0
- package/src/modules/warranty_claims/api/entitlement/route.ts +224 -0
- package/src/modules/warranty_claims/api/events/route.ts +226 -0
- package/src/modules/warranty_claims/api/external/claims/route.ts +528 -0
- package/src/modules/warranty_claims/api/lines/route.ts +275 -0
- package/src/modules/warranty_claims/api/openapi.ts +28 -0
- package/src/modules/warranty_claims/api/portal/attachments/route.ts +547 -0
- package/src/modules/warranty_claims/api/portal/claims/[id]/route.ts +195 -0
- package/src/modules/warranty_claims/api/portal/claims/[id]/shared.ts +125 -0
- package/src/modules/warranty_claims/api/portal/claims/[id]/submit/route.ts +82 -0
- package/src/modules/warranty_claims/api/portal/claims/[id]/withdraw/route.ts +87 -0
- package/src/modules/warranty_claims/api/portal/claims/route.ts +579 -0
- package/src/modules/warranty_claims/api/portal/events/route.ts +260 -0
- package/src/modules/warranty_claims/api/portal/options/route.ts +113 -0
- package/src/modules/warranty_claims/api/portal/orders/lines/route.ts +258 -0
- package/src/modules/warranty_claims/api/portal/orders/route.ts +206 -0
- package/src/modules/warranty_claims/api/portal/troubleshooting/route.ts +151 -0
- package/src/modules/warranty_claims/api/receiving/route.ts +215 -0
- package/src/modules/warranty_claims/api/registrations/route.ts +417 -0
- package/src/modules/warranty_claims/api/replacement-order/route.ts +188 -0
- package/src/modules/warranty_claims/api/return-label/route.ts +291 -0
- package/src/modules/warranty_claims/api/risk/route.ts +112 -0
- package/src/modules/warranty_claims/api/route.ts +543 -0
- package/src/modules/warranty_claims/api/sales-return/route.ts +183 -0
- package/src/modules/warranty_claims/api/settings-general/route.ts +230 -0
- package/src/modules/warranty_claims/api/stats/route.ts +306 -0
- package/src/modules/warranty_claims/api/submit/route.ts +146 -0
- package/src/modules/warranty_claims/api/transition/route.ts +144 -0
- package/src/modules/warranty_claims/api/troubleshooting-guides/route.ts +295 -0
- package/src/modules/warranty_claims/api/vendor-policies/route.ts +351 -0
- package/src/modules/warranty_claims/api/vendor-recovery/route.ts +148 -0
- package/src/modules/warranty_claims/api/vendor-recovery-suggestions/route.ts +167 -0
- package/src/modules/warranty_claims/backend/components/AiAssessButtons.tsx +389 -0
- package/src/modules/warranty_claims/backend/components/ClaimStageProgress.tsx +65 -0
- package/src/modules/warranty_claims/backend/components/ClaimStatusBadge.tsx +137 -0
- package/src/modules/warranty_claims/backend/components/ClaimsKpiStrip.tsx +169 -0
- package/src/modules/warranty_claims/backend/components/EntitlementLookupBadge.tsx +203 -0
- package/src/modules/warranty_claims/backend/components/ReceivingPanel.tsx +300 -0
- package/src/modules/warranty_claims/backend/components/ReturnLabelPanel.tsx +292 -0
- package/src/modules/warranty_claims/backend/components/TroubleshootingWalker.tsx +125 -0
- package/src/modules/warranty_claims/backend/components/VendorRecoverySuggestionsPanel.tsx +217 -0
- package/src/modules/warranty_claims/backend/components/WarrantyWorkspace.tsx +66 -0
- package/src/modules/warranty_claims/backend/components/claimReasonOptions.ts +57 -0
- package/src/modules/warranty_claims/backend/components/claimSla.tsx +127 -0
- package/src/modules/warranty_claims/backend/components/orderLookup.ts +109 -0
- package/src/modules/warranty_claims/backend/components/productLookup.tsx +433 -0
- package/src/modules/warranty_claims/backend/components/useUserDisplayNames.ts +66 -0
- package/src/modules/warranty_claims/backend/page.meta.ts +12 -0
- package/src/modules/warranty_claims/backend/page.tsx +1302 -0
- package/src/modules/warranty_claims/backend/warranty_claims/[id]/edit/page.meta.ts +12 -0
- package/src/modules/warranty_claims/backend/warranty_claims/[id]/edit/page.tsx +531 -0
- package/src/modules/warranty_claims/backend/warranty_claims/[id]/page.meta.ts +12 -0
- package/src/modules/warranty_claims/backend/warranty_claims/[id]/page.tsx +2599 -0
- package/src/modules/warranty_claims/backend/warranty_claims/create/page.meta.ts +12 -0
- package/src/modules/warranty_claims/backend/warranty_claims/create/page.tsx +1533 -0
- package/src/modules/warranty_claims/backend/warranty_claims/registrations/[id]/edit/page.meta.ts +13 -0
- package/src/modules/warranty_claims/backend/warranty_claims/registrations/[id]/edit/page.tsx +136 -0
- package/src/modules/warranty_claims/backend/warranty_claims/registrations/create/page.meta.ts +13 -0
- package/src/modules/warranty_claims/backend/warranty_claims/registrations/create/page.tsx +52 -0
- package/src/modules/warranty_claims/backend/warranty_claims/registrations/page.meta.ts +15 -0
- package/src/modules/warranty_claims/backend/warranty_claims/registrations/page.tsx +555 -0
- package/src/modules/warranty_claims/backend/warranty_claims/registrations/registrationForm.tsx +295 -0
- package/src/modules/warranty_claims/backend/warranty_claims/settings/page.meta.ts +15 -0
- package/src/modules/warranty_claims/backend/warranty_claims/settings/page.tsx +1903 -0
- package/src/modules/warranty_claims/backend/warranty_claims/troubleshooting-guides/TroubleshootingTreeBuilder.tsx +550 -0
- package/src/modules/warranty_claims/backend/warranty_claims/troubleshooting-guides/[id]/edit/page.meta.ts +15 -0
- package/src/modules/warranty_claims/backend/warranty_claims/troubleshooting-guides/[id]/edit/page.tsx +141 -0
- package/src/modules/warranty_claims/backend/warranty_claims/troubleshooting-guides/create/page.meta.ts +15 -0
- package/src/modules/warranty_claims/backend/warranty_claims/troubleshooting-guides/create/page.tsx +57 -0
- package/src/modules/warranty_claims/backend/warranty_claims/troubleshooting-guides/page.meta.ts +17 -0
- package/src/modules/warranty_claims/backend/warranty_claims/troubleshooting-guides/page.tsx +477 -0
- package/src/modules/warranty_claims/backend/warranty_claims/troubleshooting-guides/troubleshootingGuideForm.tsx +214 -0
- package/src/modules/warranty_claims/backend/warranty_claims/vendor-policies/[id]/edit/page.meta.ts +15 -0
- package/src/modules/warranty_claims/backend/warranty_claims/vendor-policies/[id]/edit/page.tsx +137 -0
- package/src/modules/warranty_claims/backend/warranty_claims/vendor-policies/create/page.meta.ts +15 -0
- package/src/modules/warranty_claims/backend/warranty_claims/vendor-policies/create/page.tsx +51 -0
- package/src/modules/warranty_claims/backend/warranty_claims/vendor-policies/page.meta.ts +17 -0
- package/src/modules/warranty_claims/backend/warranty_claims/vendor-policies/page.tsx +506 -0
- package/src/modules/warranty_claims/backend/warranty_claims/vendor-policies/vendorPolicyForm.tsx +220 -0
- package/src/modules/warranty_claims/ce.ts +44 -0
- package/src/modules/warranty_claims/commands/claim-lines.ts +1082 -0
- package/src/modules/warranty_claims/commands/claims.ts +3364 -0
- package/src/modules/warranty_claims/commands/index.ts +3 -0
- package/src/modules/warranty_claims/commands/settings.ts +198 -0
- package/src/modules/warranty_claims/commands/shared.ts +212 -0
- package/src/modules/warranty_claims/data/constants.ts +37 -0
- package/src/modules/warranty_claims/data/enrichers.ts +165 -0
- package/src/modules/warranty_claims/data/entities.ts +663 -0
- package/src/modules/warranty_claims/data/validators.ts +634 -0
- package/src/modules/warranty_claims/di.ts +34 -0
- package/src/modules/warranty_claims/encryption.ts +26 -0
- package/src/modules/warranty_claims/events.ts +81 -0
- package/src/modules/warranty_claims/extension-points.ts +16 -0
- package/src/modules/warranty_claims/frontend/[orgSlug]/portal/claims/[id]/page.meta.ts +9 -0
- package/src/modules/warranty_claims/frontend/[orgSlug]/portal/claims/[id]/page.tsx +1037 -0
- package/src/modules/warranty_claims/frontend/[orgSlug]/portal/claims/new/page.meta.ts +9 -0
- package/src/modules/warranty_claims/frontend/[orgSlug]/portal/claims/new/page.tsx +1409 -0
- package/src/modules/warranty_claims/frontend/[orgSlug]/portal/claims/page.meta.ts +16 -0
- package/src/modules/warranty_claims/frontend/[orgSlug]/portal/claims/page.tsx +304 -0
- package/src/modules/warranty_claims/i18n/de.json +1270 -0
- package/src/modules/warranty_claims/i18n/en.json +1270 -0
- package/src/modules/warranty_claims/i18n/es.json +1270 -0
- package/src/modules/warranty_claims/i18n/ko.json +1270 -0
- package/src/modules/warranty_claims/i18n/pl.json +1270 -0
- package/src/modules/warranty_claims/index.ts +13 -0
- package/src/modules/warranty_claims/lib/aiAssist.ts +677 -0
- package/src/modules/warranty_claims/lib/apiIdsFilter.ts +12 -0
- package/src/modules/warranty_claims/lib/assigneeNames.ts +184 -0
- package/src/modules/warranty_claims/lib/attachmentVisibility.ts +5 -0
- package/src/modules/warranty_claims/lib/bulkFeedback.ts +6 -0
- package/src/modules/warranty_claims/lib/businessHours.ts +333 -0
- package/src/modules/warranty_claims/lib/businessHoursForm.ts +206 -0
- package/src/modules/warranty_claims/lib/claimTypeConfig.ts +49 -0
- package/src/modules/warranty_claims/lib/decryptionSafety.ts +19 -0
- package/src/modules/warranty_claims/lib/deskFilters.ts +131 -0
- package/src/modules/warranty_claims/lib/dictionaries.ts +212 -0
- package/src/modules/warranty_claims/lib/dictionaryLabels.ts +16 -0
- package/src/modules/warranty_claims/lib/emailIntake.ts +123 -0
- package/src/modules/warranty_claims/lib/escalation.ts +83 -0
- package/src/modules/warranty_claims/lib/externalIntake.ts +362 -0
- package/src/modules/warranty_claims/lib/grading.ts +23 -0
- package/src/modules/warranty_claims/lib/listSegments.ts +19 -0
- package/src/modules/warranty_claims/lib/orderOwnership.ts +58 -0
- package/src/modules/warranty_claims/lib/portalAttachmentUpload.ts +11 -0
- package/src/modules/warranty_claims/lib/portalAttachmentValidation.ts +115 -0
- package/src/modules/warranty_claims/lib/portalClaimAccess.ts +37 -0
- package/src/modules/warranty_claims/lib/quantity.ts +15 -0
- package/src/modules/warranty_claims/lib/risk.ts +371 -0
- package/src/modules/warranty_claims/lib/settings.ts +76 -0
- package/src/modules/warranty_claims/lib/stateMachine.ts +130 -0
- package/src/modules/warranty_claims/lib/timelineFilters.ts +22 -0
- package/src/modules/warranty_claims/lib/triage.ts +366 -0
- package/src/modules/warranty_claims/lib/troubleshooting.ts +157 -0
- package/src/modules/warranty_claims/lib/tryResolve.ts +7 -0
- package/src/modules/warranty_claims/lib/vendorPolicyRecovery.ts +182 -0
- package/src/modules/warranty_claims/lib/warrantyPreview.ts +20 -0
- package/src/modules/warranty_claims/migrations/.snapshot-open-mercato.json +3525 -0
- package/src/modules/warranty_claims/migrations/Migration20260703191537_warranty_claims.ts +38 -0
- package/src/modules/warranty_claims/migrations/Migration20260704115605_warranty_claims.ts +17 -0
- package/src/modules/warranty_claims/migrations/Migration20260704230000_warranty_claims.ts +17 -0
- package/src/modules/warranty_claims/migrations/Migration20260705121601_warranty_claims.ts +37 -0
- package/src/modules/warranty_claims/migrations/Migration20260709120000_warranty_claims.ts +25 -0
- package/src/modules/warranty_claims/migrations/Migration20260710023607_warranty_claims.ts +13 -0
- package/src/modules/warranty_claims/migrations/Migration20260716203936_warranty_claims.ts +15 -0
- package/src/modules/warranty_claims/migrations/Migration20260717150000_warranty_claims.ts +13 -0
- package/src/modules/warranty_claims/migrations/Migration20260719120000_warranty_claims.ts +13 -0
- package/src/modules/warranty_claims/migrations/Migration20260813103053_warranty_claims.ts +11 -0
- package/src/modules/warranty_claims/notifications.client.ts +78 -0
- package/src/modules/warranty_claims/notifications.ts +101 -0
- package/src/modules/warranty_claims/search.ts +199 -0
- package/src/modules/warranty_claims/services/adjudicationEvaluator.ts +344 -0
- package/src/modules/warranty_claims/services/claimNumberGenerator.ts +69 -0
- package/src/modules/warranty_claims/services/entitlementResolver.ts +102 -0
- package/src/modules/warranty_claims/services/returnLabelProvider.ts +30 -0
- package/src/modules/warranty_claims/setup.ts +154 -0
- package/src/modules/warranty_claims/subscribers/auto-vendor-recovery.ts +140 -0
- package/src/modules/warranty_claims/subscribers/claim-assigned-notification.ts +54 -0
- package/src/modules/warranty_claims/subscribers/claim-customer-reply-notification.ts +68 -0
- package/src/modules/warranty_claims/subscribers/claim-status-notification.ts +100 -0
- package/src/modules/warranty_claims/subscribers/claim-submitted-notification.ts +53 -0
- package/src/modules/warranty_claims/subscribers/email-to-claim.ts +175 -0
- package/src/modules/warranty_claims/subscribers/return-shipment-tracking.ts +226 -0
- package/src/modules/warranty_claims/subscribers/vendor-recovery-reconciliation-undo.ts +28 -0
- package/src/modules/warranty_claims/subscribers/vendor-recovery-reconciliation.ts +31 -0
- package/src/modules/warranty_claims/widgets/dashboard/claims-queue/config.ts +15 -0
- package/src/modules/warranty_claims/widgets/dashboard/claims-queue/widget.client.tsx +237 -0
- package/src/modules/warranty_claims/widgets/dashboard/claims-queue/widget.ts +27 -0
- package/src/modules/warranty_claims/widgets/injection/claims-ai-trigger/widget.client.tsx +527 -0
- package/src/modules/warranty_claims/widgets/injection/claims-ai-trigger/widget.ts +37 -0
- package/src/modules/warranty_claims/widgets/injection/customer-claims-tab/widget.ts +21 -0
- package/src/modules/warranty_claims/widgets/injection/customer-claims-tab.tsx +210 -0
- package/src/modules/warranty_claims/widgets/injection/order-claims-tab/widget.ts +21 -0
- package/src/modules/warranty_claims/widgets/injection/order-claims-tab.tsx +210 -0
- package/src/modules/warranty_claims/widgets/injection-table.ts +36 -0
- package/src/modules/warranty_claims/widgets/notifications/WarrantyClaimNotificationRenderer.tsx +253 -0
- package/src/modules/warranty_claims/widgets/notifications/index.ts +1 -0
- package/src/modules/warranty_claims/workers/sla-escalation-sweep.ts +450 -0
|
@@ -0,0 +1,2577 @@
|
|
|
1
|
+
import { randomUUID } from "crypto";
|
|
2
|
+
import { LockMode } from "@mikro-orm/core";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { registerCommand } from "@open-mercato/shared/lib/commands";
|
|
5
|
+
import { withAtomicFlush } from "@open-mercato/shared/lib/commands/flush";
|
|
6
|
+
import { emitCrudSideEffects, emitCrudUndoSideEffects } from "@open-mercato/shared/lib/commands/helpers";
|
|
7
|
+
import { CrudHttpError } from "@open-mercato/shared/lib/crud/errors";
|
|
8
|
+
import { invalidateCrudCache } from "@open-mercato/shared/lib/crud/cache";
|
|
9
|
+
import { findOneWithDecryption, findWithDecryption } from "@open-mercato/shared/lib/encryption/find";
|
|
10
|
+
import { readSafeDecryptedString } from "../lib/decryptionSafety.js";
|
|
11
|
+
import { E } from "../../../generated/entities.ids.generated.js";
|
|
12
|
+
import {
|
|
13
|
+
WarrantyClaim,
|
|
14
|
+
WarrantyClaimLine
|
|
15
|
+
} from "../data/entities.js";
|
|
16
|
+
import {
|
|
17
|
+
CLAIM_FULFILLMENT_UPDATE_FIELDS,
|
|
18
|
+
CLAIM_INTAKE_UPDATE_FIELDS,
|
|
19
|
+
claimCreateSchema,
|
|
20
|
+
claimUpdateSchema,
|
|
21
|
+
assignClaimInputSchema,
|
|
22
|
+
claimCreateCreditMemoSchema,
|
|
23
|
+
claimCreateReplacementOrderSchema,
|
|
24
|
+
claimCreateSalesReturnSchema,
|
|
25
|
+
claimSetReturnLabelSchema,
|
|
26
|
+
commentClaimInputSchema,
|
|
27
|
+
transitionClaimInputSchema,
|
|
28
|
+
vendorRecoveryInputSchema
|
|
29
|
+
} from "../data/validators.js";
|
|
30
|
+
import { createLogger } from "@open-mercato/shared/lib/logger";
|
|
31
|
+
import { isAssignableStaffUser } from "../lib/assigneeNames.js";
|
|
32
|
+
import { WarrantyClaimNumberGenerator } from "../services/claimNumberGenerator.js";
|
|
33
|
+
import { emitWarrantyClaimsEvent } from "../events.js";
|
|
34
|
+
import { assertTransition, canResolveWithLineStatuses, computeHeaderRollups } from "../lib/stateMachine.js";
|
|
35
|
+
import { assertOrderBelongsToCustomer } from "../lib/orderOwnership.js";
|
|
36
|
+
import { addBusinessMillis, businessMillisBetween } from "../lib/businessHours.js";
|
|
37
|
+
import { resolveEffectiveWarrantyClaimSettings } from "../lib/settings.js";
|
|
38
|
+
import { evaluateClaimRisk } from "../lib/risk.js";
|
|
39
|
+
import {
|
|
40
|
+
WARRANTY_CLAIM_RESOURCE_KIND,
|
|
41
|
+
appendClaimEvent,
|
|
42
|
+
enforceWarrantyClaimOptimisticLock,
|
|
43
|
+
ensureOrganizationScope,
|
|
44
|
+
ensureTenantScope,
|
|
45
|
+
extractUndoPayload,
|
|
46
|
+
requireScopedClaim
|
|
47
|
+
} from "./shared.js";
|
|
48
|
+
const claimCrudEvents = {
|
|
49
|
+
module: "warranty_claims",
|
|
50
|
+
entity: "claim",
|
|
51
|
+
persistent: true,
|
|
52
|
+
buildPayload: (ctx) => {
|
|
53
|
+
const claim = ctx.entity;
|
|
54
|
+
return {
|
|
55
|
+
id: ctx.identifiers.id,
|
|
56
|
+
organizationId: ctx.identifiers.organizationId,
|
|
57
|
+
tenantId: ctx.identifiers.tenantId,
|
|
58
|
+
claimType: claim?.claimType ?? null,
|
|
59
|
+
status: claim?.status ?? null
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
const claimDeleteSchema = z.object({
|
|
64
|
+
id: z.string().uuid(),
|
|
65
|
+
organizationId: z.string().uuid().optional(),
|
|
66
|
+
tenantId: z.string().uuid().optional()
|
|
67
|
+
}).strict();
|
|
68
|
+
const submitClaimSchema = claimDeleteSchema.extend({
|
|
69
|
+
actorCustomerId: z.string().uuid().optional()
|
|
70
|
+
});
|
|
71
|
+
const escalateClaimInputSchema = z.object({
|
|
72
|
+
id: z.string().uuid(),
|
|
73
|
+
organizationId: z.string().uuid(),
|
|
74
|
+
tenantId: z.string().uuid(),
|
|
75
|
+
toLevel: z.coerce.number().int().min(1).max(1e3),
|
|
76
|
+
reassignToUserId: z.string().uuid().optional()
|
|
77
|
+
}).strict();
|
|
78
|
+
const intakeStatuses = /* @__PURE__ */ new Set(["draft", "submitted", "in_review", "info_requested"]);
|
|
79
|
+
const fulfillmentStatuses = /* @__PURE__ */ new Set(["approved", "awaiting_return", "received", "inspecting"]);
|
|
80
|
+
const deletableStatuses = /* @__PURE__ */ new Set(["draft", "cancelled"]);
|
|
81
|
+
const preReceivedStatuses = /* @__PURE__ */ new Set([
|
|
82
|
+
"draft",
|
|
83
|
+
"submitted",
|
|
84
|
+
"in_review",
|
|
85
|
+
"info_requested",
|
|
86
|
+
"approved",
|
|
87
|
+
"awaiting_return"
|
|
88
|
+
]);
|
|
89
|
+
function parseCommandInput(schema, rawInput) {
|
|
90
|
+
const result = schema.safeParse(rawInput ?? {});
|
|
91
|
+
if (!result.success) {
|
|
92
|
+
throw new CrudHttpError(400, { error: "warranty_claims.errors.invalidInput" });
|
|
93
|
+
}
|
|
94
|
+
return result.data;
|
|
95
|
+
}
|
|
96
|
+
function hasOwn(input, key) {
|
|
97
|
+
return Object.prototype.hasOwnProperty.call(input, key);
|
|
98
|
+
}
|
|
99
|
+
function resolveScope(ctx, input) {
|
|
100
|
+
const tenantId = input.tenantId ?? ctx.auth?.tenantId ?? null;
|
|
101
|
+
const organizationId = input.organizationId ?? ctx.selectedOrganizationId ?? ctx.auth?.orgId ?? null;
|
|
102
|
+
if (!tenantId) throw new CrudHttpError(400, { error: "[internal] tenant scope required for warranty claim command" });
|
|
103
|
+
if (!organizationId) throw new CrudHttpError(400, { error: "[internal] organization scope required for warranty claim command" });
|
|
104
|
+
ensureTenantScope(ctx, tenantId);
|
|
105
|
+
ensureOrganizationScope(ctx, organizationId);
|
|
106
|
+
return { tenantId, organizationId };
|
|
107
|
+
}
|
|
108
|
+
function toIso(value) {
|
|
109
|
+
if (!value) return null;
|
|
110
|
+
const date = value instanceof Date ? value : new Date(value);
|
|
111
|
+
return Number.isNaN(date.getTime()) ? null : date.toISOString();
|
|
112
|
+
}
|
|
113
|
+
function toDate(value) {
|
|
114
|
+
if (!value) return null;
|
|
115
|
+
const date = new Date(value);
|
|
116
|
+
return Number.isNaN(date.getTime()) ? null : date;
|
|
117
|
+
}
|
|
118
|
+
function toDateOnlyIso(value) {
|
|
119
|
+
const iso = toIso(value);
|
|
120
|
+
return iso ? iso.slice(0, 10) : null;
|
|
121
|
+
}
|
|
122
|
+
function toDateOnly(value) {
|
|
123
|
+
if (!value) return null;
|
|
124
|
+
const date = /* @__PURE__ */ new Date(`${value}T00:00:00.000Z`);
|
|
125
|
+
return Number.isNaN(date.getTime()) ? null : date;
|
|
126
|
+
}
|
|
127
|
+
function amountString(value, fallback = "0") {
|
|
128
|
+
if (value === null) return null;
|
|
129
|
+
if (value === void 0) return fallback;
|
|
130
|
+
const parsed = typeof value === "number" ? value : Number(value);
|
|
131
|
+
if (!Number.isFinite(parsed)) return fallback;
|
|
132
|
+
return String(parsed);
|
|
133
|
+
}
|
|
134
|
+
function nullableAmountString(value) {
|
|
135
|
+
if (value === void 0 || value === null) return null;
|
|
136
|
+
return amountString(value, "0");
|
|
137
|
+
}
|
|
138
|
+
function addMonths(date, months) {
|
|
139
|
+
const target = new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth() + months, 1));
|
|
140
|
+
const lastDay = new Date(Date.UTC(target.getUTCFullYear(), target.getUTCMonth() + 1, 0)).getUTCDate();
|
|
141
|
+
const copy = new Date(date.getTime());
|
|
142
|
+
copy.setUTCDate(1);
|
|
143
|
+
copy.setUTCFullYear(target.getUTCFullYear(), target.getUTCMonth(), Math.min(date.getUTCDate(), lastDay));
|
|
144
|
+
return copy;
|
|
145
|
+
}
|
|
146
|
+
function computeWarrantyDates(purchaseDate, warrantyMonths) {
|
|
147
|
+
if (!purchaseDate || warrantyMonths === null || warrantyMonths === void 0) {
|
|
148
|
+
return { warrantyExpiresAt: null, warrantyStatus: "unknown" };
|
|
149
|
+
}
|
|
150
|
+
const warrantyExpiresAt = addMonths(purchaseDate, warrantyMonths);
|
|
151
|
+
const warrantyStatus = warrantyExpiresAt.getTime() >= Date.now() ? "in_warranty" : "out_of_warranty";
|
|
152
|
+
return { warrantyExpiresAt, warrantyStatus };
|
|
153
|
+
}
|
|
154
|
+
function buildCreateEntitlementInput(input) {
|
|
155
|
+
const lines = input.lines ?? [];
|
|
156
|
+
const sourceLine = lines.find((line) => typeof line.serialNumber === "string" && line.serialNumber.trim().length > 0) ?? lines.find((line) => Boolean(line.productId)) ?? lines.find((line) => typeof line.sku === "string" && line.sku.trim().length > 0) ?? (input.orderId ? lines.find((line) => Boolean(line.purchaseDate)) : null) ?? null;
|
|
157
|
+
const serialNumber = sourceLine?.serialNumber?.trim() || null;
|
|
158
|
+
const orderId = input.orderId ?? null;
|
|
159
|
+
const productId = sourceLine?.productId ?? null;
|
|
160
|
+
const sku = sourceLine?.sku?.trim() || null;
|
|
161
|
+
if (!serialNumber && !orderId && !productId && !sku) return null;
|
|
162
|
+
return {
|
|
163
|
+
serialNumber,
|
|
164
|
+
orderId,
|
|
165
|
+
productId,
|
|
166
|
+
sku,
|
|
167
|
+
purchaseDate: toDateOnlyIso(sourceLine?.purchaseDate)
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
async function stampEntitlementSourceIfResolvable(ctx, em, claim, input, scope) {
|
|
171
|
+
const entitlementInput = buildCreateEntitlementInput(input);
|
|
172
|
+
if (!entitlementInput) return;
|
|
173
|
+
try {
|
|
174
|
+
const resolver = ctx.container.resolve("warrantyEntitlementResolver");
|
|
175
|
+
const result = await resolver.resolveEntitlement(entitlementInput, scope, em);
|
|
176
|
+
claim.entitlementSource = result.source ?? null;
|
|
177
|
+
} catch {
|
|
178
|
+
claim.entitlementSource = null;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
function buildInitialLineData(claim, input, index) {
|
|
182
|
+
const purchaseDate = input.purchaseDate ?? null;
|
|
183
|
+
const warrantyMonths = input.warrantyMonths ?? null;
|
|
184
|
+
const computedWarranty = computeWarrantyDates(purchaseDate, warrantyMonths);
|
|
185
|
+
return {
|
|
186
|
+
id: randomUUID(),
|
|
187
|
+
claim,
|
|
188
|
+
organizationId: claim.organizationId,
|
|
189
|
+
tenantId: claim.tenantId,
|
|
190
|
+
lineNo: input.lineNo ?? index + 1,
|
|
191
|
+
productId: input.productId ?? null,
|
|
192
|
+
variantId: input.variantId ?? null,
|
|
193
|
+
sku: input.sku ?? null,
|
|
194
|
+
productName: input.productName ?? null,
|
|
195
|
+
orderLineId: input.orderLineId ?? null,
|
|
196
|
+
serialNumber: input.serialNumber ?? null,
|
|
197
|
+
lotNumber: input.lotNumber ?? null,
|
|
198
|
+
purchaseDate,
|
|
199
|
+
warrantyMonths,
|
|
200
|
+
warrantyExpiresAt: input.warrantyExpiresAt ?? computedWarranty.warrantyExpiresAt,
|
|
201
|
+
warrantyStatus: input.warrantyStatus ?? computedWarranty.warrantyStatus,
|
|
202
|
+
faultCode: input.faultCode ?? null,
|
|
203
|
+
faultDescription: input.faultDescription ?? null,
|
|
204
|
+
qtyClaimed: amountString(input.qtyClaimed, "1") ?? "1",
|
|
205
|
+
qtyApproved: nullableAmountString(input.qtyApproved),
|
|
206
|
+
qtyReceived: nullableAmountString(input.qtyReceived),
|
|
207
|
+
conditionOnReceipt: input.conditionOnReceipt ?? null,
|
|
208
|
+
inspectionNotes: input.inspectionNotes ?? null,
|
|
209
|
+
disposition: input.disposition ?? null,
|
|
210
|
+
vendorName: input.vendorName ?? null,
|
|
211
|
+
lineStatus: "pending",
|
|
212
|
+
creditAmount: nullableAmountString(input.creditAmount),
|
|
213
|
+
restockingFee: nullableAmountString(input.restockingFee),
|
|
214
|
+
coreChargeAmount: nullableAmountString(input.coreChargeAmount),
|
|
215
|
+
coreCreditAmount: nullableAmountString(input.coreCreditAmount),
|
|
216
|
+
createdAt: /* @__PURE__ */ new Date(),
|
|
217
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
function snapshotLine(line) {
|
|
221
|
+
return {
|
|
222
|
+
id: line.id,
|
|
223
|
+
lineNo: line.lineNo,
|
|
224
|
+
productId: line.productId ?? null,
|
|
225
|
+
variantId: line.variantId ?? null,
|
|
226
|
+
sku: line.sku ?? null,
|
|
227
|
+
productName: line.productName ?? null,
|
|
228
|
+
orderLineId: line.orderLineId ?? null,
|
|
229
|
+
serialNumber: line.serialNumber ?? null,
|
|
230
|
+
lotNumber: line.lotNumber ?? null,
|
|
231
|
+
purchaseDate: toDateOnlyIso(line.purchaseDate),
|
|
232
|
+
warrantyMonths: line.warrantyMonths ?? null,
|
|
233
|
+
warrantyExpiresAt: toDateOnlyIso(line.warrantyExpiresAt),
|
|
234
|
+
warrantyStatus: line.warrantyStatus,
|
|
235
|
+
faultCode: line.faultCode ?? null,
|
|
236
|
+
faultDescription: line.faultDescription ?? null,
|
|
237
|
+
qtyClaimed: line.qtyClaimed,
|
|
238
|
+
qtyApproved: line.qtyApproved ?? null,
|
|
239
|
+
qtyReceived: line.qtyReceived ?? null,
|
|
240
|
+
conditionOnReceipt: line.conditionOnReceipt ?? null,
|
|
241
|
+
conditionGrade: line.conditionGrade ?? null,
|
|
242
|
+
quarantineStatus: line.quarantineStatus,
|
|
243
|
+
inspectionNotes: line.inspectionNotes ?? null,
|
|
244
|
+
assessmentPayload: line.assessmentPayload ?? null,
|
|
245
|
+
disposition: line.disposition ?? null,
|
|
246
|
+
lineStatus: line.lineStatus,
|
|
247
|
+
creditAmount: line.creditAmount ?? null,
|
|
248
|
+
restockingFee: line.restockingFee ?? null,
|
|
249
|
+
coreChargeAmount: line.coreChargeAmount ?? null,
|
|
250
|
+
coreCreditAmount: line.coreCreditAmount ?? null,
|
|
251
|
+
vendorClaimLineId: line.vendorClaimLineId ?? null,
|
|
252
|
+
vendorName: line.vendorName ?? null,
|
|
253
|
+
deletedAt: toIso(line.deletedAt),
|
|
254
|
+
updatedAt: toIso(line.updatedAt)
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
function snapshotClaim(claim, lines) {
|
|
258
|
+
return {
|
|
259
|
+
id: claim.id,
|
|
260
|
+
organizationId: claim.organizationId,
|
|
261
|
+
tenantId: claim.tenantId,
|
|
262
|
+
claimNumber: claim.claimNumber,
|
|
263
|
+
claimType: claim.claimType,
|
|
264
|
+
status: claim.status,
|
|
265
|
+
channel: claim.channel,
|
|
266
|
+
priority: claim.priority,
|
|
267
|
+
customerId: claim.customerId ?? null,
|
|
268
|
+
customerName: claim.customerName ?? null,
|
|
269
|
+
externalRef: claim.externalRef ?? null,
|
|
270
|
+
intakeMessageRef: claim.intakeMessageRef ?? null,
|
|
271
|
+
entitlementSource: claim.entitlementSource ?? null,
|
|
272
|
+
contactEmail: claim.contactEmail ?? null,
|
|
273
|
+
returnLabelUrl: claim.returnLabelUrl ?? null,
|
|
274
|
+
returnTrackingNumber: claim.returnTrackingNumber ?? null,
|
|
275
|
+
returnCarrier: claim.returnCarrier ?? null,
|
|
276
|
+
vendorName: claim.vendorName ?? null,
|
|
277
|
+
vendorRef: claim.vendorRef ?? null,
|
|
278
|
+
orderId: claim.orderId ?? null,
|
|
279
|
+
orderNumber: claim.orderNumber ?? null,
|
|
280
|
+
awaitingStaffReply: claim.awaitingStaffReply === true,
|
|
281
|
+
salesReturnId: claim.salesReturnId ?? null,
|
|
282
|
+
replacementOrderId: claim.replacementOrderId ?? null,
|
|
283
|
+
creditMemoId: claim.creditMemoId ?? null,
|
|
284
|
+
sourceClaimId: claim.sourceClaimId ?? null,
|
|
285
|
+
advanceReplacement: claim.advanceReplacement,
|
|
286
|
+
advanceShippedAt: toIso(claim.advanceShippedAt),
|
|
287
|
+
reasonCode: claim.reasonCode ?? null,
|
|
288
|
+
rejectionReasonCode: claim.rejectionReasonCode ?? null,
|
|
289
|
+
resolutionSummary: claim.resolutionSummary ?? null,
|
|
290
|
+
notes: claim.notes ?? null,
|
|
291
|
+
currencyCode: claim.currencyCode ?? null,
|
|
292
|
+
totalClaimedAmount: claim.totalClaimedAmount ?? null,
|
|
293
|
+
totalApprovedAmount: claim.totalApprovedAmount ?? null,
|
|
294
|
+
totalRecoveredAmount: claim.totalRecoveredAmount ?? null,
|
|
295
|
+
escalationLevel: claim.escalationLevel ?? 0,
|
|
296
|
+
escalatedAt: toIso(claim.escalatedAt),
|
|
297
|
+
slaDueAt: toIso(claim.slaDueAt),
|
|
298
|
+
slaPausedAt: toIso(claim.slaPausedAt),
|
|
299
|
+
slaAtRiskNotifiedAt: toIso(claim.slaAtRiskNotifiedAt),
|
|
300
|
+
slaBreachedNotifiedAt: toIso(claim.slaBreachedNotifiedAt),
|
|
301
|
+
submittedAt: toIso(claim.submittedAt),
|
|
302
|
+
resolvedAt: toIso(claim.resolvedAt),
|
|
303
|
+
closedAt: toIso(claim.closedAt),
|
|
304
|
+
assigneeUserId: claim.assigneeUserId ?? null,
|
|
305
|
+
deletedAt: toIso(claim.deletedAt),
|
|
306
|
+
updatedAt: toIso(claim.updatedAt),
|
|
307
|
+
lines: lines.map(snapshotLine)
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
async function loadClaimSnapshot(em, claimId, scope) {
|
|
311
|
+
const claim = await findOneWithDecryption(em, WarrantyClaim, { id: claimId, tenantId: scope.tenantId, organizationId: scope.organizationId }, {}, scope);
|
|
312
|
+
if (!claim) return null;
|
|
313
|
+
const lines = await findWithDecryption(
|
|
314
|
+
em,
|
|
315
|
+
WarrantyClaimLine,
|
|
316
|
+
{ claim: claim.id, tenantId: scope.tenantId, organizationId: scope.organizationId },
|
|
317
|
+
{},
|
|
318
|
+
scope
|
|
319
|
+
);
|
|
320
|
+
return snapshotClaim(claim, lines);
|
|
321
|
+
}
|
|
322
|
+
function snapshotValueChanged(before, after) {
|
|
323
|
+
return JSON.stringify(before) !== JSON.stringify(after);
|
|
324
|
+
}
|
|
325
|
+
function restoreClaimDelta(claim, before, after) {
|
|
326
|
+
if (before.claimNumber !== after.claimNumber) claim.claimNumber = before.claimNumber;
|
|
327
|
+
if (before.claimType !== after.claimType) claim.claimType = before.claimType;
|
|
328
|
+
if (before.status !== after.status) claim.status = before.status;
|
|
329
|
+
if (before.channel !== after.channel) claim.channel = before.channel;
|
|
330
|
+
if (before.priority !== after.priority) claim.priority = before.priority;
|
|
331
|
+
if (before.customerId !== after.customerId) claim.customerId = before.customerId;
|
|
332
|
+
if (before.customerName !== after.customerName) claim.customerName = before.customerName;
|
|
333
|
+
if (before.externalRef !== after.externalRef) claim.externalRef = before.externalRef;
|
|
334
|
+
if (before.intakeMessageRef !== after.intakeMessageRef) claim.intakeMessageRef = before.intakeMessageRef;
|
|
335
|
+
if (before.entitlementSource !== after.entitlementSource) claim.entitlementSource = before.entitlementSource;
|
|
336
|
+
if (before.contactEmail !== after.contactEmail) claim.contactEmail = before.contactEmail;
|
|
337
|
+
if (before.returnLabelUrl !== after.returnLabelUrl) claim.returnLabelUrl = before.returnLabelUrl;
|
|
338
|
+
if (before.returnTrackingNumber !== after.returnTrackingNumber) claim.returnTrackingNumber = before.returnTrackingNumber;
|
|
339
|
+
if (before.returnCarrier !== after.returnCarrier) claim.returnCarrier = before.returnCarrier;
|
|
340
|
+
if (before.vendorName !== after.vendorName) claim.vendorName = before.vendorName;
|
|
341
|
+
if (before.vendorRef !== after.vendorRef) claim.vendorRef = before.vendorRef;
|
|
342
|
+
if (before.orderId !== after.orderId) claim.orderId = before.orderId;
|
|
343
|
+
if (before.orderNumber !== after.orderNumber) claim.orderNumber = before.orderNumber;
|
|
344
|
+
if (before.awaitingStaffReply !== after.awaitingStaffReply) claim.awaitingStaffReply = before.awaitingStaffReply === true;
|
|
345
|
+
if (before.salesReturnId !== after.salesReturnId) claim.salesReturnId = before.salesReturnId;
|
|
346
|
+
if (before.replacementOrderId !== after.replacementOrderId) claim.replacementOrderId = before.replacementOrderId;
|
|
347
|
+
if (before.creditMemoId !== after.creditMemoId) claim.creditMemoId = before.creditMemoId;
|
|
348
|
+
if (before.sourceClaimId !== after.sourceClaimId) claim.sourceClaimId = before.sourceClaimId;
|
|
349
|
+
if (before.advanceReplacement !== after.advanceReplacement) claim.advanceReplacement = before.advanceReplacement;
|
|
350
|
+
if (before.advanceShippedAt !== after.advanceShippedAt) claim.advanceShippedAt = toDate(before.advanceShippedAt);
|
|
351
|
+
if (before.reasonCode !== after.reasonCode) claim.reasonCode = before.reasonCode;
|
|
352
|
+
if (before.rejectionReasonCode !== after.rejectionReasonCode) claim.rejectionReasonCode = before.rejectionReasonCode;
|
|
353
|
+
if (before.resolutionSummary !== after.resolutionSummary) claim.resolutionSummary = before.resolutionSummary;
|
|
354
|
+
if (before.notes !== after.notes) claim.notes = before.notes;
|
|
355
|
+
if (before.currencyCode !== after.currencyCode) claim.currencyCode = before.currencyCode;
|
|
356
|
+
if (before.totalClaimedAmount !== after.totalClaimedAmount) claim.totalClaimedAmount = before.totalClaimedAmount;
|
|
357
|
+
if (before.totalApprovedAmount !== after.totalApprovedAmount) claim.totalApprovedAmount = before.totalApprovedAmount;
|
|
358
|
+
if (before.totalRecoveredAmount !== after.totalRecoveredAmount) claim.totalRecoveredAmount = before.totalRecoveredAmount;
|
|
359
|
+
if (before.escalationLevel !== after.escalationLevel) claim.escalationLevel = before.escalationLevel;
|
|
360
|
+
if (before.escalatedAt !== after.escalatedAt) claim.escalatedAt = toDate(before.escalatedAt);
|
|
361
|
+
if (before.slaDueAt !== after.slaDueAt) claim.slaDueAt = toDate(before.slaDueAt);
|
|
362
|
+
if (before.slaPausedAt !== after.slaPausedAt) claim.slaPausedAt = toDate(before.slaPausedAt);
|
|
363
|
+
if (before.slaAtRiskNotifiedAt !== after.slaAtRiskNotifiedAt) claim.slaAtRiskNotifiedAt = toDate(before.slaAtRiskNotifiedAt);
|
|
364
|
+
if (before.slaBreachedNotifiedAt !== after.slaBreachedNotifiedAt) claim.slaBreachedNotifiedAt = toDate(before.slaBreachedNotifiedAt);
|
|
365
|
+
if (before.submittedAt !== after.submittedAt) claim.submittedAt = toDate(before.submittedAt);
|
|
366
|
+
if (before.resolvedAt !== after.resolvedAt) claim.resolvedAt = toDate(before.resolvedAt);
|
|
367
|
+
if (before.closedAt !== after.closedAt) claim.closedAt = toDate(before.closedAt);
|
|
368
|
+
if (before.assigneeUserId !== after.assigneeUserId) claim.assigneeUserId = before.assigneeUserId;
|
|
369
|
+
if (before.deletedAt !== after.deletedAt) claim.deletedAt = toDate(before.deletedAt);
|
|
370
|
+
claim.updatedAt = /* @__PURE__ */ new Date();
|
|
371
|
+
}
|
|
372
|
+
function restoreLineDelta(line, before, after) {
|
|
373
|
+
if (before.lineNo !== after.lineNo) line.lineNo = before.lineNo;
|
|
374
|
+
if (before.productId !== after.productId) line.productId = before.productId;
|
|
375
|
+
if (before.variantId !== after.variantId) line.variantId = before.variantId;
|
|
376
|
+
if (before.sku !== after.sku) line.sku = before.sku;
|
|
377
|
+
if (before.productName !== after.productName) line.productName = before.productName;
|
|
378
|
+
if (before.orderLineId !== after.orderLineId) line.orderLineId = before.orderLineId;
|
|
379
|
+
if (before.serialNumber !== after.serialNumber) line.serialNumber = before.serialNumber;
|
|
380
|
+
if (before.lotNumber !== after.lotNumber) line.lotNumber = before.lotNumber;
|
|
381
|
+
if (before.purchaseDate !== after.purchaseDate) line.purchaseDate = toDateOnly(before.purchaseDate);
|
|
382
|
+
if (before.warrantyMonths !== after.warrantyMonths) line.warrantyMonths = before.warrantyMonths;
|
|
383
|
+
if (before.warrantyExpiresAt !== after.warrantyExpiresAt) line.warrantyExpiresAt = toDateOnly(before.warrantyExpiresAt);
|
|
384
|
+
if (before.warrantyStatus !== after.warrantyStatus) line.warrantyStatus = before.warrantyStatus;
|
|
385
|
+
if (before.faultCode !== after.faultCode) line.faultCode = before.faultCode;
|
|
386
|
+
if (before.faultDescription !== after.faultDescription) line.faultDescription = before.faultDescription;
|
|
387
|
+
if (before.qtyClaimed !== after.qtyClaimed) line.qtyClaimed = before.qtyClaimed;
|
|
388
|
+
if (before.qtyApproved !== after.qtyApproved) line.qtyApproved = before.qtyApproved;
|
|
389
|
+
if (before.qtyReceived !== after.qtyReceived) line.qtyReceived = before.qtyReceived;
|
|
390
|
+
if (before.conditionOnReceipt !== after.conditionOnReceipt) line.conditionOnReceipt = before.conditionOnReceipt;
|
|
391
|
+
if (before.conditionGrade !== after.conditionGrade) line.conditionGrade = before.conditionGrade;
|
|
392
|
+
if (before.quarantineStatus !== after.quarantineStatus) line.quarantineStatus = before.quarantineStatus;
|
|
393
|
+
if (before.inspectionNotes !== after.inspectionNotes) line.inspectionNotes = before.inspectionNotes;
|
|
394
|
+
if (snapshotValueChanged(before.assessmentPayload, after.assessmentPayload)) line.assessmentPayload = before.assessmentPayload;
|
|
395
|
+
if (before.disposition !== after.disposition) line.disposition = before.disposition;
|
|
396
|
+
if (before.lineStatus !== after.lineStatus) line.lineStatus = before.lineStatus;
|
|
397
|
+
if (before.creditAmount !== after.creditAmount) line.creditAmount = before.creditAmount;
|
|
398
|
+
if (before.restockingFee !== after.restockingFee) line.restockingFee = before.restockingFee;
|
|
399
|
+
if (before.coreChargeAmount !== after.coreChargeAmount) line.coreChargeAmount = before.coreChargeAmount;
|
|
400
|
+
if (before.coreCreditAmount !== after.coreCreditAmount) line.coreCreditAmount = before.coreCreditAmount;
|
|
401
|
+
if (before.vendorClaimLineId !== after.vendorClaimLineId) line.vendorClaimLineId = before.vendorClaimLineId;
|
|
402
|
+
if (before.vendorName !== after.vendorName) line.vendorName = before.vendorName;
|
|
403
|
+
if (before.deletedAt !== after.deletedAt) line.deletedAt = toDate(before.deletedAt);
|
|
404
|
+
line.updatedAt = /* @__PURE__ */ new Date();
|
|
405
|
+
}
|
|
406
|
+
function throwClaimUndoConflict() {
|
|
407
|
+
throw new CrudHttpError(409, { error: "warranty_claims.errors.conflict" });
|
|
408
|
+
}
|
|
409
|
+
async function requireCurrentUndoSnapshot(em, after) {
|
|
410
|
+
const scope = { tenantId: after.tenantId, organizationId: after.organizationId };
|
|
411
|
+
const claim = await findOneWithDecryption(
|
|
412
|
+
em,
|
|
413
|
+
WarrantyClaim,
|
|
414
|
+
{ id: after.id, tenantId: scope.tenantId, organizationId: scope.organizationId },
|
|
415
|
+
{ lockMode: LockMode.PESSIMISTIC_WRITE },
|
|
416
|
+
scope
|
|
417
|
+
);
|
|
418
|
+
const expectedClaimUpdatedAt = toIso(after.updatedAt);
|
|
419
|
+
if (!claim || !expectedClaimUpdatedAt || toIso(claim.updatedAt) !== expectedClaimUpdatedAt) throwClaimUndoConflict();
|
|
420
|
+
const lines = await findWithDecryption(
|
|
421
|
+
em,
|
|
422
|
+
WarrantyClaimLine,
|
|
423
|
+
{ claim: after.id, tenantId: scope.tenantId, organizationId: scope.organizationId },
|
|
424
|
+
{ lockMode: LockMode.PESSIMISTIC_WRITE },
|
|
425
|
+
scope
|
|
426
|
+
);
|
|
427
|
+
const afterLines = Array.isArray(after.lines) ? after.lines : [];
|
|
428
|
+
if (lines.length !== afterLines.length) throwClaimUndoConflict();
|
|
429
|
+
const afterLinesById = new Map(afterLines.map((line) => [line.id, line]));
|
|
430
|
+
for (const line of lines) {
|
|
431
|
+
const expected = afterLinesById.get(line.id);
|
|
432
|
+
const expectedLineUpdatedAt = toIso(expected?.updatedAt);
|
|
433
|
+
if (!expectedLineUpdatedAt || toIso(line.updatedAt) !== expectedLineUpdatedAt) throwClaimUndoConflict();
|
|
434
|
+
}
|
|
435
|
+
return { claim, lines };
|
|
436
|
+
}
|
|
437
|
+
async function assertUndoSnapshotCurrent(ctx, after) {
|
|
438
|
+
const em = ctx.container.resolve("em").fork();
|
|
439
|
+
await em.transactional(async (tx) => {
|
|
440
|
+
await requireCurrentUndoSnapshot(tx, after);
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
async function restoreSnapshot(em, before, after) {
|
|
444
|
+
const { claim, lines } = await requireCurrentUndoSnapshot(em, after);
|
|
445
|
+
restoreClaimDelta(claim, before, after);
|
|
446
|
+
const beforeLinesById = new Map((Array.isArray(before.lines) ? before.lines : []).map((line) => [line.id, line]));
|
|
447
|
+
const afterLinesById = new Map((Array.isArray(after.lines) ? after.lines : []).map((line) => [line.id, line]));
|
|
448
|
+
for (const line of lines) {
|
|
449
|
+
const beforeLine = beforeLinesById.get(line.id);
|
|
450
|
+
const afterLine = afterLinesById.get(line.id);
|
|
451
|
+
if (!beforeLine || !afterLine) throwClaimUndoConflict();
|
|
452
|
+
if (snapshotValueChanged(beforeLine, afterLine)) restoreLineDelta(line, beforeLine, afterLine);
|
|
453
|
+
}
|
|
454
|
+
return claim;
|
|
455
|
+
}
|
|
456
|
+
function claimEventPayload(claim) {
|
|
457
|
+
return {
|
|
458
|
+
id: claim.id,
|
|
459
|
+
claimId: claim.id,
|
|
460
|
+
claimNumber: claim.claimNumber,
|
|
461
|
+
externalRef: claim.externalRef ?? null,
|
|
462
|
+
claimType: claim.claimType,
|
|
463
|
+
status: claim.status,
|
|
464
|
+
customerId: claim.customerId ?? null,
|
|
465
|
+
organizationId: claim.organizationId,
|
|
466
|
+
tenantId: claim.tenantId
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
function claimPersistentEmitOptions(claim) {
|
|
470
|
+
return {
|
|
471
|
+
persistent: true,
|
|
472
|
+
tenantId: claim.tenantId,
|
|
473
|
+
organizationId: claim.organizationId
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
async function emitClaimCrud(ctx, action, claim) {
|
|
477
|
+
const dataEngine = ctx.container.resolve("dataEngine");
|
|
478
|
+
await emitCrudSideEffects({
|
|
479
|
+
dataEngine,
|
|
480
|
+
action,
|
|
481
|
+
entity: claim,
|
|
482
|
+
identifiers: { id: claim.id, organizationId: claim.organizationId, tenantId: claim.tenantId },
|
|
483
|
+
indexer: { entityType: E.warranty_claims.warranty_claim },
|
|
484
|
+
events: claimCrudEvents
|
|
485
|
+
});
|
|
486
|
+
await invalidateCrudCache(
|
|
487
|
+
ctx.container,
|
|
488
|
+
"warranty_claims.claim",
|
|
489
|
+
{ id: claim.id, organizationId: claim.organizationId, tenantId: claim.tenantId },
|
|
490
|
+
ctx.auth?.tenantId ?? null,
|
|
491
|
+
`warranty_claims.claim.${action}`
|
|
492
|
+
);
|
|
493
|
+
}
|
|
494
|
+
async function emitClaimUndoCrud(ctx, action, claim) {
|
|
495
|
+
const dataEngine = ctx.container.resolve("dataEngine");
|
|
496
|
+
await emitCrudUndoSideEffects({
|
|
497
|
+
dataEngine,
|
|
498
|
+
action,
|
|
499
|
+
entity: claim,
|
|
500
|
+
identifiers: { id: claim.id, organizationId: claim.organizationId, tenantId: claim.tenantId },
|
|
501
|
+
indexer: { entityType: E.warranty_claims.warranty_claim },
|
|
502
|
+
events: claimCrudEvents
|
|
503
|
+
});
|
|
504
|
+
await invalidateCrudCache(
|
|
505
|
+
ctx.container,
|
|
506
|
+
"warranty_claims.claim",
|
|
507
|
+
{ id: claim.id, organizationId: claim.organizationId, tenantId: claim.tenantId },
|
|
508
|
+
ctx.auth?.tenantId ?? null,
|
|
509
|
+
`warranty_claims.claim.undo.${action}`
|
|
510
|
+
);
|
|
511
|
+
}
|
|
512
|
+
async function emitLineCrud(ctx, action, line) {
|
|
513
|
+
const dataEngine = ctx.container.resolve("dataEngine");
|
|
514
|
+
await emitCrudSideEffects({
|
|
515
|
+
dataEngine,
|
|
516
|
+
action,
|
|
517
|
+
entity: line,
|
|
518
|
+
identifiers: { id: line.id, organizationId: line.organizationId, tenantId: line.tenantId },
|
|
519
|
+
indexer: { entityType: E.warranty_claims.warranty_claim_line }
|
|
520
|
+
});
|
|
521
|
+
await invalidateCrudCache(
|
|
522
|
+
ctx.container,
|
|
523
|
+
"warranty_claims.claim_line",
|
|
524
|
+
{ id: line.id, organizationId: line.organizationId, tenantId: line.tenantId },
|
|
525
|
+
ctx.auth?.tenantId ?? null,
|
|
526
|
+
`warranty_claims.claim_line.${action}`
|
|
527
|
+
);
|
|
528
|
+
}
|
|
529
|
+
async function emitLineUndoCrud(ctx, action, line) {
|
|
530
|
+
const dataEngine = ctx.container.resolve("dataEngine");
|
|
531
|
+
await emitCrudUndoSideEffects({
|
|
532
|
+
dataEngine,
|
|
533
|
+
action,
|
|
534
|
+
entity: line,
|
|
535
|
+
identifiers: { id: line.id, organizationId: line.organizationId, tenantId: line.tenantId },
|
|
536
|
+
indexer: { entityType: E.warranty_claims.warranty_claim_line }
|
|
537
|
+
});
|
|
538
|
+
await invalidateCrudCache(
|
|
539
|
+
ctx.container,
|
|
540
|
+
"warranty_claims.claim_line",
|
|
541
|
+
{ id: line.id, organizationId: line.organizationId, tenantId: line.tenantId },
|
|
542
|
+
ctx.auth?.tenantId ?? null,
|
|
543
|
+
`warranty_claims.claim_line.undo.${action}`
|
|
544
|
+
);
|
|
545
|
+
}
|
|
546
|
+
function resolveClaimNumberGenerator(ctx, em) {
|
|
547
|
+
try {
|
|
548
|
+
return ctx.container.resolve("warrantyClaimNumberGenerator");
|
|
549
|
+
} catch {
|
|
550
|
+
return new WarrantyClaimNumberGenerator(em);
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
function readCustomerName(row) {
|
|
554
|
+
const candidates = [row.display_name, row.displayName, row.name, row.label];
|
|
555
|
+
for (const candidate of candidates) {
|
|
556
|
+
if (typeof candidate === "string" && candidate.trim().length > 0) return candidate.trim();
|
|
557
|
+
}
|
|
558
|
+
return null;
|
|
559
|
+
}
|
|
560
|
+
function readString(row, key) {
|
|
561
|
+
const value = row[key];
|
|
562
|
+
return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
|
|
563
|
+
}
|
|
564
|
+
function isMissingReferenceTableError(err) {
|
|
565
|
+
if (typeof err !== "object" || err === null) return false;
|
|
566
|
+
const candidate = err;
|
|
567
|
+
return candidate.code === "42P01" || typeof candidate.message === "string" && candidate.message.includes("does not exist");
|
|
568
|
+
}
|
|
569
|
+
async function querySalesReferenceRow(ctx, table, scope, id, select) {
|
|
570
|
+
const em = ctx.container.resolve("em").fork();
|
|
571
|
+
const db = em.getKysely();
|
|
572
|
+
try {
|
|
573
|
+
const row = await db.selectFrom(table).select(select).where("id", "=", id).where("tenant_id", "=", scope.tenantId).where("organization_id", "=", scope.organizationId).where("deleted_at", "is", null).executeTakeFirst();
|
|
574
|
+
return row ?? null;
|
|
575
|
+
} catch (err) {
|
|
576
|
+
if (isMissingReferenceTableError(err)) return void 0;
|
|
577
|
+
throw err;
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
async function querySalesOrderLineReferences(ctx, scope, ids, select = ["id", "order_id"]) {
|
|
581
|
+
if (ids.length === 0) return [];
|
|
582
|
+
const em = ctx.container.resolve("em").fork();
|
|
583
|
+
const db = em.getKysely();
|
|
584
|
+
try {
|
|
585
|
+
const rows = await db.selectFrom("sales_order_lines").select(select).where("id", "in", ids).where("tenant_id", "=", scope.tenantId).where("organization_id", "=", scope.organizationId).where("deleted_at", "is", null).execute();
|
|
586
|
+
return rows;
|
|
587
|
+
} catch (err) {
|
|
588
|
+
if (isMissingReferenceTableError(err)) return void 0;
|
|
589
|
+
throw err;
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
const CLAIMED_QUANTITY_SCALE = 10000n;
|
|
593
|
+
const TAX_RATE_FORMULA_SCALE = 1000000n;
|
|
594
|
+
function claimedQuantityUnits(value) {
|
|
595
|
+
if (value === null || value === void 0) return null;
|
|
596
|
+
let normalized = String(value).trim();
|
|
597
|
+
if (/e/i.test(normalized)) {
|
|
598
|
+
const numeric = Number(normalized);
|
|
599
|
+
if (!Number.isFinite(numeric)) return null;
|
|
600
|
+
normalized = numeric.toFixed(4);
|
|
601
|
+
}
|
|
602
|
+
const match = /^([+-]?)(\d+)(?:\.(\d+))?$/.exec(normalized);
|
|
603
|
+
if (!match) return null;
|
|
604
|
+
const [, sign, whole, fraction = ""] = match;
|
|
605
|
+
const scaledFraction = fraction.slice(0, 4).padEnd(4, "0");
|
|
606
|
+
let units = BigInt(whole) * CLAIMED_QUANTITY_SCALE + BigInt(scaledFraction);
|
|
607
|
+
if (fraction.length > 4 && fraction[4] >= "5") units += 1n;
|
|
608
|
+
return sign === "-" ? -units : units;
|
|
609
|
+
}
|
|
610
|
+
function roundHalfUpDivision(numerator, denominator) {
|
|
611
|
+
if (denominator <= 0n) throw new Error("[internal] Positive denominator required");
|
|
612
|
+
if (numerator < 0n) return -roundHalfUpDivision(-numerator, denominator);
|
|
613
|
+
return (numerator + denominator / 2n) / denominator;
|
|
614
|
+
}
|
|
615
|
+
function formatScaledUnits(units) {
|
|
616
|
+
const sign = units < 0n ? "-" : "";
|
|
617
|
+
const absolute = units < 0n ? -units : units;
|
|
618
|
+
const whole = absolute / CLAIMED_QUANTITY_SCALE;
|
|
619
|
+
const fraction = String(absolute % CLAIMED_QUANTITY_SCALE).padStart(4, "0");
|
|
620
|
+
return `${sign}${whole}.${fraction}`;
|
|
621
|
+
}
|
|
622
|
+
function netUnitsFromGross(grossUnits, taxRateUnits) {
|
|
623
|
+
const denominator = TAX_RATE_FORMULA_SCALE + taxRateUnits;
|
|
624
|
+
return roundHalfUpDivision(grossUnits * TAX_RATE_FORMULA_SCALE, denominator);
|
|
625
|
+
}
|
|
626
|
+
async function assertClaimedQtyWithinSold(ctx, scope, claimIdOrPendingLines, candidateLine) {
|
|
627
|
+
const orderLineId = candidateLine.orderLineId ?? null;
|
|
628
|
+
if (!orderLineId || !resolveOptionalEntityId("sales", "sales_order_line")) return;
|
|
629
|
+
const salesLine = await querySalesReferenceRow(ctx, "sales_order_lines", scope, orderLineId, ["id", "quantity"]);
|
|
630
|
+
const soldQuantity = claimedQuantityUnits(salesLine?.quantity);
|
|
631
|
+
if (salesLine === void 0 || salesLine === null || soldQuantity === null) return;
|
|
632
|
+
let lines;
|
|
633
|
+
if (typeof claimIdOrPendingLines === "string") {
|
|
634
|
+
const em = ctx.container.resolve("em").fork();
|
|
635
|
+
lines = await findWithDecryption(
|
|
636
|
+
em,
|
|
637
|
+
WarrantyClaimLine,
|
|
638
|
+
{
|
|
639
|
+
claim: claimIdOrPendingLines,
|
|
640
|
+
orderLineId,
|
|
641
|
+
tenantId: scope.tenantId,
|
|
642
|
+
organizationId: scope.organizationId,
|
|
643
|
+
deletedAt: null
|
|
644
|
+
},
|
|
645
|
+
{},
|
|
646
|
+
scope
|
|
647
|
+
);
|
|
648
|
+
} else {
|
|
649
|
+
lines = claimIdOrPendingLines;
|
|
650
|
+
}
|
|
651
|
+
let claimedQuantity = claimedQuantityUnits(candidateLine.qtyClaimed) ?? 0n;
|
|
652
|
+
for (const line of lines) {
|
|
653
|
+
if (line.deletedAt || (line.orderLineId ?? null) !== orderLineId) continue;
|
|
654
|
+
if (candidateLine.id != null && line.id === candidateLine.id) continue;
|
|
655
|
+
claimedQuantity += claimedQuantityUnits(line.qtyClaimed) ?? 0n;
|
|
656
|
+
}
|
|
657
|
+
if (claimedQuantity > soldQuantity) {
|
|
658
|
+
throw new CrudHttpError(400, { error: "warranty_claims.errors.qtyExceedsOrdered" });
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
async function assertPendingClaimQuantitiesWithinSold(em, scope, pendingLinesByOrderLine, options) {
|
|
662
|
+
const db = em.getKysely();
|
|
663
|
+
const orderLineIds = Array.from(pendingLinesByOrderLine.keys()).sort((left, right) => left.localeCompare(right));
|
|
664
|
+
for (const orderLineId of orderLineIds) {
|
|
665
|
+
let soldRow;
|
|
666
|
+
try {
|
|
667
|
+
soldRow = await db.selectFrom("sales_order_lines").select(["id", "quantity"]).where("id", "=", orderLineId).where("tenant_id", "=", scope.tenantId).where("organization_id", "=", scope.organizationId).where("deleted_at", "is", null).forUpdate().executeTakeFirst();
|
|
668
|
+
} catch (err) {
|
|
669
|
+
if (isMissingReferenceTableError(err)) return;
|
|
670
|
+
throw err;
|
|
671
|
+
}
|
|
672
|
+
const soldQuantity = claimedQuantityUnits(soldRow?.quantity);
|
|
673
|
+
if (!soldRow || soldQuantity === null) continue;
|
|
674
|
+
const persistedRows = await db.selectFrom("warranty_claim_lines").innerJoin("warranty_claims", "warranty_claims.id", "warranty_claim_lines.claim_id").select(["warranty_claim_lines.id", "warranty_claim_lines.qty_claimed"]).where("warranty_claim_lines.order_line_id", "=", orderLineId).where("warranty_claim_lines.tenant_id", "=", scope.tenantId).where("warranty_claim_lines.organization_id", "=", scope.organizationId).where("warranty_claim_lines.deleted_at", "is", null).where("warranty_claim_lines.line_status", "!=", "rejected").where("warranty_claims.tenant_id", "=", scope.tenantId).where("warranty_claims.organization_id", "=", scope.organizationId).where("warranty_claims.id", "=", options.currentClaimId).where("warranty_claims.deleted_at", "is", null).where("warranty_claims.status", "!=", "cancelled").execute();
|
|
675
|
+
let claimedQuantity = 0n;
|
|
676
|
+
for (const row of persistedRows) {
|
|
677
|
+
if (options.excludedLineIds?.has(row.id)) continue;
|
|
678
|
+
claimedQuantity += claimedQuantityUnits(row.qty_claimed) ?? 0n;
|
|
679
|
+
}
|
|
680
|
+
for (const line of pendingLinesByOrderLine.get(orderLineId) ?? []) {
|
|
681
|
+
claimedQuantity += claimedQuantityUnits(line.qtyClaimed) ?? 0n;
|
|
682
|
+
}
|
|
683
|
+
if (claimedQuantity > soldQuantity) {
|
|
684
|
+
throw new CrudHttpError(400, { error: "warranty_claims.errors.qtyExceedsOrdered" });
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
function isMissingCustomerTableError(err) {
|
|
689
|
+
if (typeof err !== "object" || err === null) return false;
|
|
690
|
+
const candidate = err;
|
|
691
|
+
return candidate.code === "42P01" || typeof candidate.message === "string" && candidate.message.includes("customer_entities") && candidate.message.includes("does not exist");
|
|
692
|
+
}
|
|
693
|
+
async function assertCustomerExists(ctx, customerId, scope) {
|
|
694
|
+
const em = ctx.container.resolve("em").fork();
|
|
695
|
+
const db = em.getKysely();
|
|
696
|
+
let row;
|
|
697
|
+
try {
|
|
698
|
+
row = await db.selectFrom("customer_entities").select("id").where("id", "=", customerId).where("tenant_id", "=", scope.tenantId).where("organization_id", "=", scope.organizationId).where("deleted_at", "is", null).executeTakeFirst();
|
|
699
|
+
} catch (err) {
|
|
700
|
+
if (isMissingCustomerTableError(err)) return;
|
|
701
|
+
throw err;
|
|
702
|
+
}
|
|
703
|
+
if (!row) throw new CrudHttpError(400, { error: "warranty_claims.errors.invalidInput" });
|
|
704
|
+
}
|
|
705
|
+
async function resolveCustomerName(ctx, customerId, scope, fallback, options = { strict: false }) {
|
|
706
|
+
if (!customerId) return fallback ?? null;
|
|
707
|
+
if (options.strict) await assertCustomerExists(ctx, customerId, scope);
|
|
708
|
+
const em = ctx.container.resolve("em").fork();
|
|
709
|
+
const db = em.getKysely();
|
|
710
|
+
let customer;
|
|
711
|
+
try {
|
|
712
|
+
customer = await db.selectFrom("customer_entities").select(["id", "display_name"]).where("id", "=", customerId).where("tenant_id", "=", scope.tenantId).where("organization_id", "=", scope.organizationId).where("deleted_at", "is", null).executeTakeFirst();
|
|
713
|
+
} catch (err) {
|
|
714
|
+
if (!isMissingCustomerTableError(err)) throw err;
|
|
715
|
+
return fallback ?? null;
|
|
716
|
+
}
|
|
717
|
+
if (!customer) return fallback ?? null;
|
|
718
|
+
let encryption = null;
|
|
719
|
+
try {
|
|
720
|
+
encryption = ctx.container.resolve("tenantEncryptionService");
|
|
721
|
+
} catch {
|
|
722
|
+
encryption = null;
|
|
723
|
+
}
|
|
724
|
+
if (encryption) {
|
|
725
|
+
try {
|
|
726
|
+
customer = await encryption.decryptEntityPayload("customers:customer_entity", customer, scope.tenantId, scope.organizationId);
|
|
727
|
+
} catch {
|
|
728
|
+
return fallback ?? null;
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
return readSafeDecryptedString(readCustomerName(customer)) ?? fallback ?? null;
|
|
732
|
+
}
|
|
733
|
+
async function resolveOrderNumber(ctx, orderId, scope, fallback) {
|
|
734
|
+
if (!orderId) return null;
|
|
735
|
+
const row = await querySalesReferenceRow(ctx, "sales_orders", scope, orderId, ["id", "order_number"]);
|
|
736
|
+
if (!row) return fallback ?? null;
|
|
737
|
+
return readString(row, "order_number") ?? fallback ?? null;
|
|
738
|
+
}
|
|
739
|
+
async function resolvePortalRecipientUserIds(ctx, claim) {
|
|
740
|
+
if (!claim.customerId) return [];
|
|
741
|
+
try {
|
|
742
|
+
const em = ctx.container.resolve("em").fork();
|
|
743
|
+
const db = em.getKysely();
|
|
744
|
+
const rows = await db.selectFrom("customer_users").select("id").where("tenant_id", "=", claim.tenantId).where("organization_id", "=", claim.organizationId).where("customer_entity_id", "=", claim.customerId).where("is_active", "=", true).where("deleted_at", "is", null).limit(100).execute();
|
|
745
|
+
return rows.map((row) => row.id);
|
|
746
|
+
} catch {
|
|
747
|
+
return [];
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
async function emitClaimStatusChanged(ctx, claim, fromStatus, toStatus) {
|
|
751
|
+
const payload = {
|
|
752
|
+
...claimEventPayload(claim),
|
|
753
|
+
fromStatus,
|
|
754
|
+
toStatus
|
|
755
|
+
};
|
|
756
|
+
await emitWarrantyClaimsEvent("warranty_claims.claim.status_changed", payload, claimPersistentEmitOptions(claim));
|
|
757
|
+
const recipientUserIds = await resolvePortalRecipientUserIds(ctx, claim);
|
|
758
|
+
if (recipientUserIds.length === 0) return;
|
|
759
|
+
await emitWarrantyClaimsEvent("warranty_claims.claim.portal_status_changed", {
|
|
760
|
+
...payload,
|
|
761
|
+
recipientUserIds
|
|
762
|
+
}, claimPersistentEmitOptions(claim));
|
|
763
|
+
}
|
|
764
|
+
async function emitCustomerVisibleCommentAdded(ctx, claim, input) {
|
|
765
|
+
const recipientUserIds = await resolvePortalRecipientUserIds(ctx, claim);
|
|
766
|
+
if (recipientUserIds.length === 0) return;
|
|
767
|
+
await emitWarrantyClaimsEvent("warranty_claims.claim.comment_added", {
|
|
768
|
+
...claimEventPayload(claim),
|
|
769
|
+
visibility: input.visibility,
|
|
770
|
+
actorCustomerId: input.actorCustomerId ?? null,
|
|
771
|
+
recipientUserIds
|
|
772
|
+
}, claimPersistentEmitOptions(claim));
|
|
773
|
+
}
|
|
774
|
+
function applySlaPause(em, claim, fromStatus, toStatus, now, effectiveSettings) {
|
|
775
|
+
if (fromStatus === toStatus || toStatus !== "info_requested" || !effectiveSettings.slaPauseOnInfoRequested) return;
|
|
776
|
+
claim.slaPausedAt = now;
|
|
777
|
+
appendClaimEvent(em, claim, "system", {
|
|
778
|
+
visibility: "customer",
|
|
779
|
+
payload: { action: "sla_paused" },
|
|
780
|
+
actorUserId: null
|
|
781
|
+
});
|
|
782
|
+
}
|
|
783
|
+
function applySlaResume(em, claim, fromStatus, toStatus, now, effectiveSettings) {
|
|
784
|
+
if (fromStatus !== "info_requested" || toStatus === "info_requested" || !claim.slaPausedAt) return;
|
|
785
|
+
if (claim.slaDueAt && claim.slaDueAt.getTime() > claim.slaPausedAt.getTime()) {
|
|
786
|
+
const remainingBusinessMillis = businessMillisBetween(claim.slaPausedAt, claim.slaDueAt, effectiveSettings.businessHours);
|
|
787
|
+
claim.slaDueAt = addBusinessMillis(now, remainingBusinessMillis, effectiveSettings.businessHours);
|
|
788
|
+
}
|
|
789
|
+
claim.slaPausedAt = null;
|
|
790
|
+
claim.slaAtRiskNotifiedAt = null;
|
|
791
|
+
claim.slaBreachedNotifiedAt = null;
|
|
792
|
+
appendClaimEvent(em, claim, "system", {
|
|
793
|
+
visibility: "customer",
|
|
794
|
+
payload: { action: "sla_resumed" },
|
|
795
|
+
actorUserId: null
|
|
796
|
+
});
|
|
797
|
+
}
|
|
798
|
+
function resolveOptionalEntityId(moduleId, entity) {
|
|
799
|
+
const registry = E;
|
|
800
|
+
const value = registry[moduleId]?.[entity];
|
|
801
|
+
return typeof value === "string" ? value : null;
|
|
802
|
+
}
|
|
803
|
+
async function lookupSalesReference(ctx, entityId, table, scope, id, select) {
|
|
804
|
+
if (!entityId) return "unknown";
|
|
805
|
+
const row = await querySalesReferenceRow(ctx, table, scope, id, select);
|
|
806
|
+
if (row === void 0) return "unknown";
|
|
807
|
+
return row ?? "missing";
|
|
808
|
+
}
|
|
809
|
+
function assertValidSalesReference(result) {
|
|
810
|
+
if (result === "missing") {
|
|
811
|
+
throw new CrudHttpError(400, { error: "warranty_claims.errors.invalidReference" });
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
async function validateClaimReferences(ctx, scope, input) {
|
|
815
|
+
if (input.orderId) {
|
|
816
|
+
const result = await lookupSalesReference(ctx, resolveOptionalEntityId("sales", "sales_order"), "sales_orders", scope, input.orderId, ["id"]);
|
|
817
|
+
assertValidSalesReference(result);
|
|
818
|
+
}
|
|
819
|
+
if (input.replacementOrderId) {
|
|
820
|
+
const result = await lookupSalesReference(ctx, resolveOptionalEntityId("sales", "sales_order"), "sales_orders", scope, input.replacementOrderId, ["id"]);
|
|
821
|
+
assertValidSalesReference(result);
|
|
822
|
+
}
|
|
823
|
+
if (input.salesReturnId) {
|
|
824
|
+
const result = await lookupSalesReference(ctx, resolveOptionalEntityId("sales", "sales_return"), "sales_returns", scope, input.salesReturnId, ["id"]);
|
|
825
|
+
assertValidSalesReference(result);
|
|
826
|
+
}
|
|
827
|
+
if (input.creditMemoId) {
|
|
828
|
+
const result = await lookupSalesReference(ctx, resolveOptionalEntityId("sales", "sales_credit_memo"), "sales_credit_memos", scope, input.creditMemoId, ["id"]);
|
|
829
|
+
assertValidSalesReference(result);
|
|
830
|
+
}
|
|
831
|
+
const lineRefs = input.lineOrderRefs ?? [];
|
|
832
|
+
if (lineRefs.length === 0 || !resolveOptionalEntityId("sales", "sales_order_line")) return;
|
|
833
|
+
const lineRows = await querySalesOrderLineReferences(
|
|
834
|
+
ctx,
|
|
835
|
+
scope,
|
|
836
|
+
Array.from(new Set(lineRefs.map((lineRef) => lineRef.orderLineId)))
|
|
837
|
+
);
|
|
838
|
+
if (lineRows === void 0) return;
|
|
839
|
+
const lineRowsById = new Map(lineRows.map((row) => [readString(row, "id"), row]));
|
|
840
|
+
for (const lineRef of lineRefs) {
|
|
841
|
+
const result = lineRowsById.get(lineRef.orderLineId);
|
|
842
|
+
if (!result) {
|
|
843
|
+
throw new CrudHttpError(400, { error: "warranty_claims.errors.invalidReference" });
|
|
844
|
+
}
|
|
845
|
+
if (lineRef.orderId && readString(result, "order_id") !== lineRef.orderId) {
|
|
846
|
+
throw new CrudHttpError(400, { error: "warranty_claims.errors.invalidReference" });
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
async function validateAssigneeUser(ctx, scope, assigneeUserId) {
|
|
851
|
+
if (!assigneeUserId) return;
|
|
852
|
+
const assignable = await isAssignableStaffUser({
|
|
853
|
+
container: ctx.container,
|
|
854
|
+
tenantId: scope.tenantId,
|
|
855
|
+
organizationId: scope.organizationId
|
|
856
|
+
}, assigneeUserId);
|
|
857
|
+
if (!assignable) throw new CrudHttpError(400, { error: "warranty_claims.errors.invalidAssignee" });
|
|
858
|
+
}
|
|
859
|
+
function applyClaimAssignment(em, claim, assigneeUserId, actorUserId, now) {
|
|
860
|
+
claim.assigneeUserId = assigneeUserId;
|
|
861
|
+
claim.updatedAt = now;
|
|
862
|
+
appendClaimEvent(em, claim, "assignment", {
|
|
863
|
+
visibility: "internal",
|
|
864
|
+
payload: { assigneeUserId: claim.assigneeUserId },
|
|
865
|
+
actorUserId
|
|
866
|
+
});
|
|
867
|
+
}
|
|
868
|
+
function allowedUpdateFields(status) {
|
|
869
|
+
if (intakeStatuses.has(status)) return new Set(CLAIM_INTAKE_UPDATE_FIELDS);
|
|
870
|
+
if (fulfillmentStatuses.has(status)) return new Set(CLAIM_FULFILLMENT_UPDATE_FIELDS);
|
|
871
|
+
return /* @__PURE__ */ new Set();
|
|
872
|
+
}
|
|
873
|
+
function assertClaimUpdateFields(claim, input) {
|
|
874
|
+
const keys = [...CLAIM_INTAKE_UPDATE_FIELDS, ...CLAIM_FULFILLMENT_UPDATE_FIELDS];
|
|
875
|
+
const requested = keys.filter((key) => hasOwn(input, key));
|
|
876
|
+
if (!requested.length) return;
|
|
877
|
+
const allowed = allowedUpdateFields(claim.status);
|
|
878
|
+
const disallowed = requested.filter((key) => !allowed.has(key));
|
|
879
|
+
if (disallowed.length > 0) {
|
|
880
|
+
throw new CrudHttpError(400, { error: "warranty_claims.errors.fieldLocked" });
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
function applyClaimUpdate(claim, input, customerName, orderNumber) {
|
|
884
|
+
if (hasOwn(input, "customerId")) claim.customerId = input.customerId ?? null;
|
|
885
|
+
if (hasOwn(input, "customerName")) claim.customerName = customerName;
|
|
886
|
+
if (hasOwn(input, "orderId")) {
|
|
887
|
+
claim.orderId = input.orderId ?? null;
|
|
888
|
+
claim.orderNumber = orderNumber;
|
|
889
|
+
}
|
|
890
|
+
if (hasOwn(input, "reasonCode")) claim.reasonCode = input.reasonCode ?? null;
|
|
891
|
+
if (hasOwn(input, "priority") && input.priority) claim.priority = input.priority;
|
|
892
|
+
if (hasOwn(input, "notes")) claim.notes = input.notes ?? null;
|
|
893
|
+
if (hasOwn(input, "advanceReplacement")) claim.advanceReplacement = input.advanceReplacement ?? false;
|
|
894
|
+
if (hasOwn(input, "replacementOrderId")) claim.replacementOrderId = input.replacementOrderId ?? null;
|
|
895
|
+
if (hasOwn(input, "advanceShippedAt")) claim.advanceShippedAt = input.advanceShippedAt ?? null;
|
|
896
|
+
if (hasOwn(input, "salesReturnId")) claim.salesReturnId = input.salesReturnId ?? null;
|
|
897
|
+
if (hasOwn(input, "creditMemoId")) claim.creditMemoId = input.creditMemoId ?? null;
|
|
898
|
+
if (hasOwn(input, "vendorName")) claim.vendorName = input.vendorName ?? null;
|
|
899
|
+
if (hasOwn(input, "vendorRef")) claim.vendorRef = input.vendorRef ?? null;
|
|
900
|
+
if (hasOwn(input, "resolutionSummary")) claim.resolutionSummary = input.resolutionSummary ?? null;
|
|
901
|
+
claim.updatedAt = /* @__PURE__ */ new Date();
|
|
902
|
+
}
|
|
903
|
+
async function recomputeClaimRollups(em, claim) {
|
|
904
|
+
const lines = await findWithDecryption(
|
|
905
|
+
em,
|
|
906
|
+
WarrantyClaimLine,
|
|
907
|
+
{
|
|
908
|
+
claim: claim.id,
|
|
909
|
+
tenantId: claim.tenantId,
|
|
910
|
+
organizationId: claim.organizationId,
|
|
911
|
+
deletedAt: null
|
|
912
|
+
},
|
|
913
|
+
{},
|
|
914
|
+
{ tenantId: claim.tenantId, organizationId: claim.organizationId }
|
|
915
|
+
);
|
|
916
|
+
const totals = computeHeaderRollups(lines, { claimType: claim.claimType });
|
|
917
|
+
claim.totalClaimedAmount = String(totals.totalClaimedAmount);
|
|
918
|
+
claim.totalApprovedAmount = String(totals.totalApprovedAmount);
|
|
919
|
+
}
|
|
920
|
+
function assertCanResolve(lines) {
|
|
921
|
+
if (!canResolveWithLineStatuses(lines)) {
|
|
922
|
+
throw new CrudHttpError(400, { error: "warranty_claims.errors.invalidTransition" });
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
function assertVendorRecoveryLines(lines, requestedLineIds) {
|
|
926
|
+
if (lines.length !== requestedLineIds.length) {
|
|
927
|
+
throw new CrudHttpError(400, { error: "warranty_claims.errors.vendorRecoveryNeedsResolvedLines" });
|
|
928
|
+
}
|
|
929
|
+
for (const line of lines) {
|
|
930
|
+
if (line.lineStatus !== "resolved" || line.vendorClaimLineId) {
|
|
931
|
+
throw new CrudHttpError(400, { error: "warranty_claims.errors.vendorRecoveryNeedsResolvedLines" });
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
function buildClaimLog(actionLabel, resourceId, snapshots) {
|
|
936
|
+
const after = snapshots.after;
|
|
937
|
+
const before = snapshots.before;
|
|
938
|
+
return {
|
|
939
|
+
actionLabel,
|
|
940
|
+
resourceKind: WARRANTY_CLAIM_RESOURCE_KIND,
|
|
941
|
+
resourceId,
|
|
942
|
+
tenantId: after?.tenantId ?? before?.tenantId ?? null,
|
|
943
|
+
organizationId: after?.organizationId ?? before?.organizationId ?? null,
|
|
944
|
+
snapshotBefore: before ?? null,
|
|
945
|
+
snapshotAfter: after ?? null,
|
|
946
|
+
payload: { undo: { before: before ?? null, after: after ?? null } }
|
|
947
|
+
};
|
|
948
|
+
}
|
|
949
|
+
const createClaimCommand = {
|
|
950
|
+
id: "warranty_claims.claim.create",
|
|
951
|
+
isUndoable: true,
|
|
952
|
+
async execute(rawInput, ctx) {
|
|
953
|
+
const input = parseCommandInput(claimCreateSchema, rawInput);
|
|
954
|
+
const scope = resolveScope(ctx, input);
|
|
955
|
+
const em = ctx.container.resolve("em").fork();
|
|
956
|
+
await validateClaimReferences(ctx, scope, {
|
|
957
|
+
orderId: input.orderId ?? null,
|
|
958
|
+
salesReturnId: input.salesReturnId ?? null,
|
|
959
|
+
replacementOrderId: input.replacementOrderId ?? null,
|
|
960
|
+
creditMemoId: null,
|
|
961
|
+
lineOrderRefs: (input.lines ?? []).filter((line) => typeof line.orderLineId === "string").map((line) => ({ orderLineId: line.orderLineId, orderId: input.orderId ?? null }))
|
|
962
|
+
});
|
|
963
|
+
await assertOrderBelongsToCustomer(em, scope, input.orderId ?? null, input.customerId ?? null);
|
|
964
|
+
const pendingLines = (input.lines ?? []).map((line) => ({
|
|
965
|
+
orderLineId: line.orderLineId ?? null,
|
|
966
|
+
qtyClaimed: amountString(line.qtyClaimed, "1") ?? "1"
|
|
967
|
+
}));
|
|
968
|
+
const pendingLinesByOrderLine = /* @__PURE__ */ new Map();
|
|
969
|
+
for (const line of pendingLines) {
|
|
970
|
+
if (!line.orderLineId) continue;
|
|
971
|
+
const grouped = pendingLinesByOrderLine.get(line.orderLineId) ?? [];
|
|
972
|
+
grouped.push(line);
|
|
973
|
+
pendingLinesByOrderLine.set(line.orderLineId, grouped);
|
|
974
|
+
}
|
|
975
|
+
const numberGenerator = resolveClaimNumberGenerator(ctx, em);
|
|
976
|
+
const generated = await numberGenerator.generate({
|
|
977
|
+
claimType: input.claimType,
|
|
978
|
+
tenantId: scope.tenantId,
|
|
979
|
+
organizationId: scope.organizationId
|
|
980
|
+
});
|
|
981
|
+
const customerName = await resolveCustomerName(ctx, input.customerId, scope, input.customerName, { strict: true });
|
|
982
|
+
const orderNumber = input.orderId ? await resolveOrderNumber(ctx, input.orderId, scope, input.orderNumber ?? null) : input.orderNumber ?? null;
|
|
983
|
+
const claimId = randomUUID();
|
|
984
|
+
let claim;
|
|
985
|
+
let createdLines = [];
|
|
986
|
+
await withAtomicFlush(em, [
|
|
987
|
+
async () => {
|
|
988
|
+
await assertPendingClaimQuantitiesWithinSold(em, scope, pendingLinesByOrderLine, { currentClaimId: claimId });
|
|
989
|
+
claim = em.create(WarrantyClaim, {
|
|
990
|
+
id: claimId,
|
|
991
|
+
organizationId: scope.organizationId,
|
|
992
|
+
tenantId: scope.tenantId,
|
|
993
|
+
claimNumber: generated.number,
|
|
994
|
+
claimType: input.claimType,
|
|
995
|
+
status: "draft",
|
|
996
|
+
channel: input.channel ?? "staff",
|
|
997
|
+
priority: input.priority ?? "normal",
|
|
998
|
+
customerId: input.customerId ?? null,
|
|
999
|
+
customerName,
|
|
1000
|
+
externalRef: input.externalRef ?? null,
|
|
1001
|
+
intakeMessageRef: input.intakeMessageRef ?? null,
|
|
1002
|
+
contactEmail: input.contactEmail ?? null,
|
|
1003
|
+
vendorName: input.vendorName ?? null,
|
|
1004
|
+
vendorRef: input.vendorRef ?? null,
|
|
1005
|
+
orderId: input.orderId ?? null,
|
|
1006
|
+
orderNumber,
|
|
1007
|
+
salesReturnId: input.salesReturnId ?? null,
|
|
1008
|
+
replacementOrderId: input.replacementOrderId ?? null,
|
|
1009
|
+
creditMemoId: null,
|
|
1010
|
+
sourceClaimId: null,
|
|
1011
|
+
advanceReplacement: input.advanceReplacement ?? false,
|
|
1012
|
+
advanceShippedAt: input.advanceShippedAt ?? null,
|
|
1013
|
+
reasonCode: input.reasonCode ?? null,
|
|
1014
|
+
rejectionReasonCode: input.rejectionReasonCode ?? null,
|
|
1015
|
+
resolutionSummary: input.resolutionSummary ?? null,
|
|
1016
|
+
notes: input.notes ?? null,
|
|
1017
|
+
currencyCode: input.currencyCode ?? null,
|
|
1018
|
+
totalClaimedAmount: "0",
|
|
1019
|
+
totalApprovedAmount: "0",
|
|
1020
|
+
totalRecoveredAmount: "0",
|
|
1021
|
+
createdAt: /* @__PURE__ */ new Date(),
|
|
1022
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
1023
|
+
});
|
|
1024
|
+
em.persist(claim);
|
|
1025
|
+
createdLines = (input.lines ?? []).map((lineInput, index) => {
|
|
1026
|
+
const line = em.create(WarrantyClaimLine, buildInitialLineData(claim, lineInput, index));
|
|
1027
|
+
em.persist(line);
|
|
1028
|
+
return line;
|
|
1029
|
+
});
|
|
1030
|
+
const totals = computeHeaderRollups(createdLines, { claimType: claim.claimType });
|
|
1031
|
+
claim.totalClaimedAmount = String(totals.totalClaimedAmount);
|
|
1032
|
+
claim.totalApprovedAmount = String(totals.totalApprovedAmount);
|
|
1033
|
+
appendClaimEvent(em, claim, "system", {
|
|
1034
|
+
visibility: "internal",
|
|
1035
|
+
payload: { action: "created" },
|
|
1036
|
+
actorUserId: ctx.auth?.sub ?? null
|
|
1037
|
+
});
|
|
1038
|
+
},
|
|
1039
|
+
() => stampEntitlementSourceIfResolvable(ctx, em, claim, input, scope)
|
|
1040
|
+
], { transaction: true, label: "warranty_claims.claim.create" });
|
|
1041
|
+
await emitClaimCrud(ctx, "created", claim);
|
|
1042
|
+
await Promise.all(createdLines.map((line) => emitLineCrud(ctx, "created", line)));
|
|
1043
|
+
return { claimId: claim.id };
|
|
1044
|
+
},
|
|
1045
|
+
captureAfter: async (rawInput, result, ctx) => {
|
|
1046
|
+
const input = parseCommandInput(claimCreateSchema, rawInput);
|
|
1047
|
+
const scope = resolveScope(ctx, input);
|
|
1048
|
+
const em = ctx.container.resolve("em").fork();
|
|
1049
|
+
return loadClaimSnapshot(em, result.claimId, scope);
|
|
1050
|
+
},
|
|
1051
|
+
buildLog: async ({ result, snapshots }) => buildClaimLog("warranty_claims.audit.claim.create", result.claimId, snapshots),
|
|
1052
|
+
undo: async ({ logEntry, ctx }) => {
|
|
1053
|
+
const payload = extractUndoPayload(logEntry);
|
|
1054
|
+
const after = payload?.after;
|
|
1055
|
+
if (!after) return;
|
|
1056
|
+
const em = ctx.container.resolve("em").fork();
|
|
1057
|
+
let claim;
|
|
1058
|
+
let lines = [];
|
|
1059
|
+
await withAtomicFlush(em, [
|
|
1060
|
+
async () => {
|
|
1061
|
+
const current = await requireCurrentUndoSnapshot(em, after);
|
|
1062
|
+
claim = current.claim;
|
|
1063
|
+
lines = current.lines.filter((line) => !line.deletedAt);
|
|
1064
|
+
const now = /* @__PURE__ */ new Date();
|
|
1065
|
+
claim.deletedAt = now;
|
|
1066
|
+
claim.updatedAt = now;
|
|
1067
|
+
for (const line of lines) {
|
|
1068
|
+
line.deletedAt = now;
|
|
1069
|
+
line.updatedAt = now;
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
], { transaction: true, label: "warranty_claims.claim.create.undo" });
|
|
1073
|
+
await emitClaimUndoCrud(ctx, "deleted", claim);
|
|
1074
|
+
await Promise.all(lines.map((line) => emitLineUndoCrud(ctx, "deleted", line)));
|
|
1075
|
+
}
|
|
1076
|
+
};
|
|
1077
|
+
const updateClaimCommand = {
|
|
1078
|
+
id: "warranty_claims.claim.update",
|
|
1079
|
+
isUndoable: true,
|
|
1080
|
+
prepare: async (rawInput, ctx) => {
|
|
1081
|
+
const input = parseCommandInput(claimUpdateSchema, rawInput);
|
|
1082
|
+
const scope = resolveScope(ctx, input);
|
|
1083
|
+
const em = ctx.container.resolve("em").fork();
|
|
1084
|
+
return { before: await loadClaimSnapshot(em, input.id, scope) };
|
|
1085
|
+
},
|
|
1086
|
+
async execute(rawInput, ctx) {
|
|
1087
|
+
const input = parseCommandInput(claimUpdateSchema, rawInput);
|
|
1088
|
+
const scope = resolveScope(ctx, input);
|
|
1089
|
+
const em = ctx.container.resolve("em").fork();
|
|
1090
|
+
const claim = await requireScopedClaim(em, input.id, scope);
|
|
1091
|
+
await enforceWarrantyClaimOptimisticLock(ctx, claim);
|
|
1092
|
+
assertClaimUpdateFields(claim, input);
|
|
1093
|
+
await validateClaimReferences(ctx, scope, {
|
|
1094
|
+
orderId: hasOwn(input, "orderId") && (input.orderId ?? null) !== (claim.orderId ?? null) ? input.orderId ?? null : null,
|
|
1095
|
+
salesReturnId: hasOwn(input, "salesReturnId") && (input.salesReturnId ?? null) !== (claim.salesReturnId ?? null) ? input.salesReturnId ?? null : null,
|
|
1096
|
+
replacementOrderId: hasOwn(input, "replacementOrderId") && (input.replacementOrderId ?? null) !== (claim.replacementOrderId ?? null) ? input.replacementOrderId ?? null : null,
|
|
1097
|
+
creditMemoId: hasOwn(input, "creditMemoId") && (input.creditMemoId ?? null) !== (claim.creditMemoId ?? null) ? input.creditMemoId ?? null : null
|
|
1098
|
+
});
|
|
1099
|
+
const effectiveOrderId = hasOwn(input, "orderId") ? input.orderId ?? null : claim.orderId ?? null;
|
|
1100
|
+
const effectiveCustomerId = hasOwn(input, "customerId") ? input.customerId ?? null : claim.customerId ?? null;
|
|
1101
|
+
await assertOrderBelongsToCustomer(em, scope, effectiveOrderId, effectiveCustomerId);
|
|
1102
|
+
const customerChanged = hasOwn(input, "customerId") && (input.customerId ?? null) !== (claim.customerId ?? null);
|
|
1103
|
+
const shouldRefreshCustomerName = hasOwn(input, "customerId") || hasOwn(input, "customerName");
|
|
1104
|
+
const customerName = shouldRefreshCustomerName ? await resolveCustomerName(ctx, input.customerId ?? claim.customerId, scope, input.customerName ?? claim.customerName, { strict: customerChanged }) : claim.customerName ?? null;
|
|
1105
|
+
const orderNumber = hasOwn(input, "orderId") ? await resolveOrderNumber(ctx, input.orderId, scope, (input.orderId ?? null) === (claim.orderId ?? null) ? claim.orderNumber : null) : claim.orderNumber ?? null;
|
|
1106
|
+
await withAtomicFlush(em, [
|
|
1107
|
+
() => applyClaimUpdate(claim, input, customerName, orderNumber)
|
|
1108
|
+
], { transaction: true, label: "warranty_claims.claim.update" });
|
|
1109
|
+
await emitClaimCrud(ctx, "updated", claim);
|
|
1110
|
+
return { claimId: claim.id };
|
|
1111
|
+
},
|
|
1112
|
+
captureAfter: async (rawInput, result, ctx) => {
|
|
1113
|
+
const input = parseCommandInput(claimUpdateSchema, rawInput);
|
|
1114
|
+
const scope = resolveScope(ctx, input);
|
|
1115
|
+
const em = ctx.container.resolve("em").fork();
|
|
1116
|
+
return loadClaimSnapshot(em, result.claimId, scope);
|
|
1117
|
+
},
|
|
1118
|
+
buildLog: async ({ result, snapshots }) => buildClaimLog("warranty_claims.audit.claim.update", result.claimId, snapshots),
|
|
1119
|
+
undo: async ({ logEntry, ctx }) => {
|
|
1120
|
+
const payload = extractUndoPayload(logEntry);
|
|
1121
|
+
const before = payload?.before;
|
|
1122
|
+
if (!before) return;
|
|
1123
|
+
const after = payload?.after;
|
|
1124
|
+
if (!after) throwClaimUndoConflict();
|
|
1125
|
+
const em = ctx.container.resolve("em").fork();
|
|
1126
|
+
let claim;
|
|
1127
|
+
await withAtomicFlush(em, [
|
|
1128
|
+
async () => {
|
|
1129
|
+
claim = await restoreSnapshot(em, before, after);
|
|
1130
|
+
}
|
|
1131
|
+
], { transaction: true, label: "warranty_claims.claim.update.undo" });
|
|
1132
|
+
await emitClaimUndoCrud(ctx, "updated", claim);
|
|
1133
|
+
}
|
|
1134
|
+
};
|
|
1135
|
+
const deleteClaimCommand = {
|
|
1136
|
+
id: "warranty_claims.claim.delete",
|
|
1137
|
+
isUndoable: true,
|
|
1138
|
+
prepare: async (rawInput, ctx) => {
|
|
1139
|
+
const input = parseCommandInput(claimDeleteSchema, rawInput);
|
|
1140
|
+
const scope = resolveScope(ctx, input);
|
|
1141
|
+
const em = ctx.container.resolve("em").fork();
|
|
1142
|
+
return { before: await loadClaimSnapshot(em, input.id, scope) };
|
|
1143
|
+
},
|
|
1144
|
+
async execute(rawInput, ctx) {
|
|
1145
|
+
const input = parseCommandInput(claimDeleteSchema, rawInput);
|
|
1146
|
+
const scope = resolveScope(ctx, input);
|
|
1147
|
+
const em = ctx.container.resolve("em").fork();
|
|
1148
|
+
const claim = await requireScopedClaim(em, input.id, scope);
|
|
1149
|
+
await enforceWarrantyClaimOptimisticLock(ctx, claim);
|
|
1150
|
+
if (!deletableStatuses.has(claim.status)) {
|
|
1151
|
+
throw new CrudHttpError(400, { error: "warranty_claims.errors.deleteNotAllowed" });
|
|
1152
|
+
}
|
|
1153
|
+
const lines = await findWithDecryption(
|
|
1154
|
+
em,
|
|
1155
|
+
WarrantyClaimLine,
|
|
1156
|
+
{ claim: claim.id, tenantId: scope.tenantId, organizationId: scope.organizationId, deletedAt: null },
|
|
1157
|
+
{},
|
|
1158
|
+
scope
|
|
1159
|
+
);
|
|
1160
|
+
await withAtomicFlush(em, [
|
|
1161
|
+
() => {
|
|
1162
|
+
const now = /* @__PURE__ */ new Date();
|
|
1163
|
+
claim.deletedAt = now;
|
|
1164
|
+
claim.updatedAt = now;
|
|
1165
|
+
for (const line of lines) {
|
|
1166
|
+
line.deletedAt = now;
|
|
1167
|
+
line.updatedAt = now;
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
], { transaction: true, label: "warranty_claims.claim.delete" });
|
|
1171
|
+
await emitClaimCrud(ctx, "deleted", claim);
|
|
1172
|
+
await Promise.all(lines.map((line) => emitLineCrud(ctx, "deleted", line)));
|
|
1173
|
+
return { claimId: claim.id };
|
|
1174
|
+
},
|
|
1175
|
+
captureAfter: async (rawInput, result, ctx) => {
|
|
1176
|
+
const input = parseCommandInput(claimDeleteSchema, rawInput);
|
|
1177
|
+
const scope = resolveScope(ctx, input);
|
|
1178
|
+
const em = ctx.container.resolve("em").fork();
|
|
1179
|
+
return loadClaimSnapshot(em, result.claimId, scope);
|
|
1180
|
+
},
|
|
1181
|
+
buildLog: async ({ result, snapshots }) => buildClaimLog("warranty_claims.audit.claim.delete", result.claimId, snapshots),
|
|
1182
|
+
undo: async ({ logEntry, ctx }) => {
|
|
1183
|
+
const payload = extractUndoPayload(logEntry);
|
|
1184
|
+
const before = payload?.before;
|
|
1185
|
+
if (!before) return;
|
|
1186
|
+
const after = payload?.after;
|
|
1187
|
+
if (!after) throwClaimUndoConflict();
|
|
1188
|
+
const em = ctx.container.resolve("em").fork();
|
|
1189
|
+
let claim;
|
|
1190
|
+
await withAtomicFlush(em, [
|
|
1191
|
+
async () => {
|
|
1192
|
+
claim = await restoreSnapshot(em, before, after);
|
|
1193
|
+
}
|
|
1194
|
+
], { transaction: true, label: "warranty_claims.claim.delete.undo" });
|
|
1195
|
+
await emitClaimUndoCrud(ctx, "created", claim);
|
|
1196
|
+
const restoredLines = await findWithDecryption(
|
|
1197
|
+
em,
|
|
1198
|
+
WarrantyClaimLine,
|
|
1199
|
+
{ claim: claim.id, tenantId: before.tenantId, organizationId: before.organizationId, deletedAt: null },
|
|
1200
|
+
{},
|
|
1201
|
+
{ tenantId: before.tenantId, organizationId: before.organizationId }
|
|
1202
|
+
);
|
|
1203
|
+
await Promise.all(restoredLines.map((line) => emitLineCrud(ctx, "created", line)));
|
|
1204
|
+
}
|
|
1205
|
+
};
|
|
1206
|
+
const submitClaimCommand = {
|
|
1207
|
+
id: "warranty_claims.claim.submit",
|
|
1208
|
+
async execute(rawInput, ctx) {
|
|
1209
|
+
const input = parseCommandInput(submitClaimSchema, rawInput);
|
|
1210
|
+
const scope = resolveScope(ctx, input);
|
|
1211
|
+
const em = ctx.container.resolve("em").fork();
|
|
1212
|
+
const claim = await requireScopedClaim(em, input.id, scope);
|
|
1213
|
+
await enforceWarrantyClaimOptimisticLock(ctx, claim);
|
|
1214
|
+
if (claim.status !== "draft") {
|
|
1215
|
+
throw new CrudHttpError(400, { error: "warranty_claims.errors.invalidTransition" });
|
|
1216
|
+
}
|
|
1217
|
+
const fromStatus = claim.status;
|
|
1218
|
+
let autoApproved = false;
|
|
1219
|
+
let effectiveSettings = null;
|
|
1220
|
+
await withAtomicFlush(em, [
|
|
1221
|
+
async () => {
|
|
1222
|
+
effectiveSettings = await resolveEffectiveWarrantyClaimSettings(em, scope);
|
|
1223
|
+
const submittedAt = /* @__PURE__ */ new Date();
|
|
1224
|
+
claim.status = "submitted";
|
|
1225
|
+
claim.submittedAt = submittedAt;
|
|
1226
|
+
claim.slaDueAt = addBusinessMillis(submittedAt, effectiveSettings.slaHours * 60 * 60 * 1e3, effectiveSettings.businessHours);
|
|
1227
|
+
claim.slaAtRiskNotifiedAt = null;
|
|
1228
|
+
claim.slaBreachedNotifiedAt = null;
|
|
1229
|
+
claim.updatedAt = submittedAt;
|
|
1230
|
+
appendClaimEvent(em, claim, "status_changed", {
|
|
1231
|
+
visibility: "customer",
|
|
1232
|
+
payload: { from: fromStatus, to: "submitted" },
|
|
1233
|
+
actorUserId: input.actorCustomerId ? null : ctx.auth?.sub ?? null,
|
|
1234
|
+
actorCustomerId: input.actorCustomerId ?? null
|
|
1235
|
+
});
|
|
1236
|
+
},
|
|
1237
|
+
async () => {
|
|
1238
|
+
const lines = await findWithDecryption(
|
|
1239
|
+
em,
|
|
1240
|
+
WarrantyClaimLine,
|
|
1241
|
+
{
|
|
1242
|
+
claim: claim.id,
|
|
1243
|
+
tenantId: claim.tenantId,
|
|
1244
|
+
organizationId: claim.organizationId,
|
|
1245
|
+
deletedAt: null
|
|
1246
|
+
},
|
|
1247
|
+
{},
|
|
1248
|
+
scope
|
|
1249
|
+
);
|
|
1250
|
+
const risk = await evaluateClaimRisk(em, claim, lines);
|
|
1251
|
+
const settings = effectiveSettings ?? await resolveEffectiveWarrantyClaimSettings(em, scope);
|
|
1252
|
+
const evaluator = ctx.container.resolve("warrantyAdjudicationEvaluator");
|
|
1253
|
+
const { decision } = await evaluator.evaluate({
|
|
1254
|
+
claim,
|
|
1255
|
+
lines,
|
|
1256
|
+
settings,
|
|
1257
|
+
risk,
|
|
1258
|
+
container: ctx.container,
|
|
1259
|
+
em,
|
|
1260
|
+
scope
|
|
1261
|
+
});
|
|
1262
|
+
if (decision === "auto_approve") {
|
|
1263
|
+
claim.status = "approved";
|
|
1264
|
+
claim.updatedAt = /* @__PURE__ */ new Date();
|
|
1265
|
+
autoApproved = true;
|
|
1266
|
+
appendClaimEvent(em, claim, "status_changed", {
|
|
1267
|
+
visibility: "customer",
|
|
1268
|
+
payload: { from: "submitted", to: "approved" },
|
|
1269
|
+
actorUserId: null
|
|
1270
|
+
});
|
|
1271
|
+
appendClaimEvent(em, claim, "system", {
|
|
1272
|
+
visibility: "internal",
|
|
1273
|
+
payload: {
|
|
1274
|
+
action: "auto_approved",
|
|
1275
|
+
maxAmount: settings.autoApproveMaxAmount,
|
|
1276
|
+
currencyCode: settings.autoApproveCurrencyCode
|
|
1277
|
+
},
|
|
1278
|
+
actorUserId: null
|
|
1279
|
+
});
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
], { transaction: true, label: "warranty_claims.claim.submit" });
|
|
1283
|
+
await emitClaimCrud(ctx, "updated", claim);
|
|
1284
|
+
const payload = claimEventPayload(claim);
|
|
1285
|
+
await emitWarrantyClaimsEvent("warranty_claims.claim.submitted", payload, claimPersistentEmitOptions(claim));
|
|
1286
|
+
await emitClaimStatusChanged(ctx, claim, autoApproved ? "submitted" : fromStatus, autoApproved ? "approved" : "submitted");
|
|
1287
|
+
return { claimId: claim.id };
|
|
1288
|
+
}
|
|
1289
|
+
};
|
|
1290
|
+
const transitionClaimCommand = {
|
|
1291
|
+
id: "warranty_claims.claim.transition",
|
|
1292
|
+
isUndoable: true,
|
|
1293
|
+
prepare: async (rawInput, ctx) => {
|
|
1294
|
+
const input = parseCommandInput(transitionClaimInputSchema, rawInput);
|
|
1295
|
+
const scope = resolveScope(ctx, {});
|
|
1296
|
+
const em = ctx.container.resolve("em").fork();
|
|
1297
|
+
return { before: await loadClaimSnapshot(em, input.id, scope) };
|
|
1298
|
+
},
|
|
1299
|
+
async execute(rawInput, ctx) {
|
|
1300
|
+
const input = parseCommandInput(transitionClaimInputSchema, rawInput);
|
|
1301
|
+
const scope = resolveScope(ctx, {});
|
|
1302
|
+
const em = ctx.container.resolve("em").fork();
|
|
1303
|
+
const claim = await requireScopedClaim(em, input.id, scope);
|
|
1304
|
+
await enforceWarrantyClaimOptimisticLock(ctx, claim);
|
|
1305
|
+
const fromStatus = claim.status;
|
|
1306
|
+
assertTransition(fromStatus, input.toStatus);
|
|
1307
|
+
if (input.toStatus === "cancelled" && !preReceivedStatuses.has(fromStatus)) {
|
|
1308
|
+
throw new CrudHttpError(400, { error: "warranty_claims.errors.invalidTransition" });
|
|
1309
|
+
}
|
|
1310
|
+
if (input.toStatus === "rejected" && !input.rejectionReasonCode) {
|
|
1311
|
+
throw new CrudHttpError(400, { error: "warranty_claims.errors.invalidTransition" });
|
|
1312
|
+
}
|
|
1313
|
+
const lines = input.toStatus === "resolved" ? await findWithDecryption(
|
|
1314
|
+
em,
|
|
1315
|
+
WarrantyClaimLine,
|
|
1316
|
+
{
|
|
1317
|
+
claim: claim.id,
|
|
1318
|
+
tenantId: claim.tenantId,
|
|
1319
|
+
organizationId: claim.organizationId,
|
|
1320
|
+
deletedAt: null
|
|
1321
|
+
},
|
|
1322
|
+
{},
|
|
1323
|
+
{ tenantId: claim.tenantId, organizationId: claim.organizationId }
|
|
1324
|
+
) : [];
|
|
1325
|
+
if (input.toStatus === "resolved") assertCanResolve(lines);
|
|
1326
|
+
await withAtomicFlush(em, [
|
|
1327
|
+
async () => {
|
|
1328
|
+
const effectiveSettings = await resolveEffectiveWarrantyClaimSettings(em, scope);
|
|
1329
|
+
const now = /* @__PURE__ */ new Date();
|
|
1330
|
+
claim.status = input.toStatus;
|
|
1331
|
+
if (input.toStatus === "rejected") claim.rejectionReasonCode = input.rejectionReasonCode ?? null;
|
|
1332
|
+
if (input.toStatus === "resolved") {
|
|
1333
|
+
claim.resolvedAt = now;
|
|
1334
|
+
if (hasOwn(input, "resolutionSummary")) claim.resolutionSummary = input.resolutionSummary ?? null;
|
|
1335
|
+
}
|
|
1336
|
+
if (input.toStatus === "closed") claim.closedAt = now;
|
|
1337
|
+
applySlaResume(em, claim, fromStatus, input.toStatus, now, effectiveSettings);
|
|
1338
|
+
applySlaPause(em, claim, fromStatus, input.toStatus, now, effectiveSettings);
|
|
1339
|
+
claim.awaitingStaffReply = false;
|
|
1340
|
+
claim.updatedAt = now;
|
|
1341
|
+
appendClaimEvent(em, claim, "status_changed", {
|
|
1342
|
+
visibility: "customer",
|
|
1343
|
+
payload: {
|
|
1344
|
+
from: fromStatus,
|
|
1345
|
+
to: input.toStatus,
|
|
1346
|
+
...input.systemNote ? { systemNote: input.systemNote } : {}
|
|
1347
|
+
},
|
|
1348
|
+
actorUserId: input.actorCustomerId ? null : ctx.auth?.sub ?? null,
|
|
1349
|
+
actorCustomerId: input.actorCustomerId ?? null
|
|
1350
|
+
});
|
|
1351
|
+
}
|
|
1352
|
+
], { transaction: true, label: "warranty_claims.claim.transition" });
|
|
1353
|
+
await emitClaimCrud(ctx, "updated", claim);
|
|
1354
|
+
await emitClaimStatusChanged(ctx, claim, fromStatus, input.toStatus);
|
|
1355
|
+
return { claimId: claim.id };
|
|
1356
|
+
},
|
|
1357
|
+
captureAfter: async (_rawInput, result, ctx) => {
|
|
1358
|
+
const scope = resolveScope(ctx, {});
|
|
1359
|
+
const em = ctx.container.resolve("em").fork();
|
|
1360
|
+
return loadClaimSnapshot(em, result.claimId, scope);
|
|
1361
|
+
},
|
|
1362
|
+
buildLog: async ({ result, snapshots }) => buildClaimLog("warranty_claims.audit.claim.transition", result.claimId, snapshots),
|
|
1363
|
+
undo: async ({ logEntry, ctx }) => {
|
|
1364
|
+
const payload = extractUndoPayload(logEntry);
|
|
1365
|
+
const before = payload?.before;
|
|
1366
|
+
if (!before) return;
|
|
1367
|
+
const after = payload?.after;
|
|
1368
|
+
if (!after) throwClaimUndoConflict();
|
|
1369
|
+
const em = ctx.container.resolve("em").fork();
|
|
1370
|
+
let claim;
|
|
1371
|
+
await withAtomicFlush(em, [
|
|
1372
|
+
async () => {
|
|
1373
|
+
claim = await restoreSnapshot(em, before, after);
|
|
1374
|
+
appendClaimEvent(em, claim, "system", {
|
|
1375
|
+
visibility: "internal",
|
|
1376
|
+
payload: { action: "undo_status_transition" },
|
|
1377
|
+
actorUserId: ctx.auth?.sub ?? null
|
|
1378
|
+
});
|
|
1379
|
+
}
|
|
1380
|
+
], { transaction: true, label: "warranty_claims.claim.transition.undo" });
|
|
1381
|
+
await emitClaimUndoCrud(ctx, "updated", claim);
|
|
1382
|
+
}
|
|
1383
|
+
};
|
|
1384
|
+
const assignClaimCommand = {
|
|
1385
|
+
id: "warranty_claims.claim.assign",
|
|
1386
|
+
async execute(rawInput, ctx) {
|
|
1387
|
+
const input = parseCommandInput(assignClaimInputSchema, rawInput);
|
|
1388
|
+
const scope = resolveScope(ctx, {});
|
|
1389
|
+
const em = ctx.container.resolve("em").fork();
|
|
1390
|
+
const claim = await requireScopedClaim(em, input.id, scope);
|
|
1391
|
+
await enforceWarrantyClaimOptimisticLock(ctx, claim);
|
|
1392
|
+
await validateAssigneeUser(ctx, scope, input.assigneeUserId ?? null);
|
|
1393
|
+
await withAtomicFlush(em, [
|
|
1394
|
+
() => {
|
|
1395
|
+
applyClaimAssignment(em, claim, input.assigneeUserId ?? null, ctx.auth?.sub ?? null, /* @__PURE__ */ new Date());
|
|
1396
|
+
}
|
|
1397
|
+
], { transaction: true, label: "warranty_claims.claim.assign" });
|
|
1398
|
+
await emitClaimCrud(ctx, "updated", claim);
|
|
1399
|
+
await emitWarrantyClaimsEvent("warranty_claims.claim.assigned", {
|
|
1400
|
+
...claimEventPayload(claim),
|
|
1401
|
+
assigneeUserId: claim.assigneeUserId
|
|
1402
|
+
}, claimPersistentEmitOptions(claim));
|
|
1403
|
+
return { claimId: claim.id };
|
|
1404
|
+
}
|
|
1405
|
+
};
|
|
1406
|
+
const setReturnLabelCommand = {
|
|
1407
|
+
id: "warranty_claims.claim.set_return_label",
|
|
1408
|
+
isUndoable: true,
|
|
1409
|
+
prepare: async (rawInput, ctx) => {
|
|
1410
|
+
const input = parseCommandInput(claimSetReturnLabelSchema, rawInput);
|
|
1411
|
+
const scope = resolveScope(ctx, input);
|
|
1412
|
+
const em = ctx.container.resolve("em").fork();
|
|
1413
|
+
return { before: await loadClaimSnapshot(em, input.id, scope) };
|
|
1414
|
+
},
|
|
1415
|
+
async execute(rawInput, ctx) {
|
|
1416
|
+
const input = parseCommandInput(claimSetReturnLabelSchema, rawInput);
|
|
1417
|
+
const scope = resolveScope(ctx, input);
|
|
1418
|
+
const em = ctx.container.resolve("em").fork();
|
|
1419
|
+
const claim = await requireScopedClaim(em, input.id, scope);
|
|
1420
|
+
await enforceWarrantyClaimOptimisticLock(ctx, claim, WARRANTY_CLAIM_RESOURCE_KIND, input.updatedAt ?? void 0);
|
|
1421
|
+
await withAtomicFlush(em, [
|
|
1422
|
+
() => {
|
|
1423
|
+
if (input.labelUrl !== void 0) claim.returnLabelUrl = input.labelUrl;
|
|
1424
|
+
if (input.trackingNumber !== void 0) claim.returnTrackingNumber = input.trackingNumber;
|
|
1425
|
+
if (input.carrier !== void 0) claim.returnCarrier = input.carrier;
|
|
1426
|
+
claim.updatedAt = /* @__PURE__ */ new Date();
|
|
1427
|
+
appendClaimEvent(em, claim, "system", {
|
|
1428
|
+
visibility: "internal",
|
|
1429
|
+
payload: {
|
|
1430
|
+
action: "return_label_created",
|
|
1431
|
+
carrier: claim.returnCarrier ?? null,
|
|
1432
|
+
trackingNumber: claim.returnTrackingNumber ?? null
|
|
1433
|
+
},
|
|
1434
|
+
actorUserId: ctx.auth?.sub ?? null
|
|
1435
|
+
});
|
|
1436
|
+
}
|
|
1437
|
+
], { transaction: true, label: "warranty_claims.claim.set_return_label" });
|
|
1438
|
+
await emitClaimCrud(ctx, "updated", claim);
|
|
1439
|
+
await emitWarrantyClaimsEvent("warranty_claims.claim.return_label_created", {
|
|
1440
|
+
id: claim.id,
|
|
1441
|
+
claimId: claim.id,
|
|
1442
|
+
carrier: claim.returnCarrier ?? null,
|
|
1443
|
+
trackingNumber: claim.returnTrackingNumber ?? null,
|
|
1444
|
+
scope,
|
|
1445
|
+
organizationId: scope.organizationId,
|
|
1446
|
+
tenantId: scope.tenantId
|
|
1447
|
+
}, claimPersistentEmitOptions(claim));
|
|
1448
|
+
return {
|
|
1449
|
+
claimId: claim.id,
|
|
1450
|
+
labelUrl: claim.returnLabelUrl ?? null,
|
|
1451
|
+
trackingNumber: claim.returnTrackingNumber ?? null,
|
|
1452
|
+
carrier: claim.returnCarrier ?? null
|
|
1453
|
+
};
|
|
1454
|
+
},
|
|
1455
|
+
captureAfter: async (rawInput, result, ctx) => {
|
|
1456
|
+
const input = parseCommandInput(claimSetReturnLabelSchema, rawInput);
|
|
1457
|
+
const scope = resolveScope(ctx, input);
|
|
1458
|
+
const em = ctx.container.resolve("em").fork();
|
|
1459
|
+
return loadClaimSnapshot(em, result.claimId, scope);
|
|
1460
|
+
},
|
|
1461
|
+
buildLog: async ({ result, snapshots }) => buildClaimLog("warranty_claims.audit.claim.set_return_label", result.claimId, snapshots),
|
|
1462
|
+
undo: async ({ logEntry, ctx }) => {
|
|
1463
|
+
const payload = extractUndoPayload(logEntry);
|
|
1464
|
+
const before = payload?.before;
|
|
1465
|
+
if (!before) return;
|
|
1466
|
+
const after = payload?.after;
|
|
1467
|
+
if (!after) throwClaimUndoConflict();
|
|
1468
|
+
const em = ctx.container.resolve("em").fork();
|
|
1469
|
+
let claim;
|
|
1470
|
+
await withAtomicFlush(em, [
|
|
1471
|
+
async () => {
|
|
1472
|
+
claim = await restoreSnapshot(em, before, after);
|
|
1473
|
+
appendClaimEvent(em, claim, "system", {
|
|
1474
|
+
visibility: "internal",
|
|
1475
|
+
payload: { action: "undo_return_label" },
|
|
1476
|
+
actorUserId: ctx.auth?.sub ?? null
|
|
1477
|
+
});
|
|
1478
|
+
}
|
|
1479
|
+
], { transaction: true, label: "warranty_claims.claim.set_return_label.undo" });
|
|
1480
|
+
await emitClaimUndoCrud(ctx, "updated", claim);
|
|
1481
|
+
}
|
|
1482
|
+
};
|
|
1483
|
+
const escalateClaimCommand = {
|
|
1484
|
+
id: "warranty_claims.claim.escalate",
|
|
1485
|
+
async execute(rawInput, ctx) {
|
|
1486
|
+
const input = parseCommandInput(escalateClaimInputSchema, rawInput);
|
|
1487
|
+
const scope = resolveScope(ctx, input);
|
|
1488
|
+
const em = ctx.container.resolve("em").fork();
|
|
1489
|
+
if (input.reassignToUserId) {
|
|
1490
|
+
await validateAssigneeUser(ctx, scope, input.reassignToUserId);
|
|
1491
|
+
}
|
|
1492
|
+
let claim = null;
|
|
1493
|
+
let escalated = false;
|
|
1494
|
+
await withAtomicFlush(em, [
|
|
1495
|
+
async () => {
|
|
1496
|
+
claim = await requireScopedClaim(em, input.id, scope, { lockMode: LockMode.PESSIMISTIC_WRITE });
|
|
1497
|
+
if ((claim.escalationLevel ?? 0) >= input.toLevel) return;
|
|
1498
|
+
const now = /* @__PURE__ */ new Date();
|
|
1499
|
+
claim.escalationLevel = input.toLevel;
|
|
1500
|
+
claim.escalatedAt = now;
|
|
1501
|
+
claim.updatedAt = now;
|
|
1502
|
+
appendClaimEvent(em, claim, "system", {
|
|
1503
|
+
visibility: "internal",
|
|
1504
|
+
payload: {
|
|
1505
|
+
action: "sla_escalated",
|
|
1506
|
+
level: input.toLevel,
|
|
1507
|
+
reassignToUserId: input.reassignToUserId ?? null
|
|
1508
|
+
},
|
|
1509
|
+
actorUserId: ctx.auth?.sub ?? null
|
|
1510
|
+
});
|
|
1511
|
+
if (input.reassignToUserId) {
|
|
1512
|
+
applyClaimAssignment(em, claim, input.reassignToUserId, ctx.auth?.sub ?? null, now);
|
|
1513
|
+
}
|
|
1514
|
+
escalated = true;
|
|
1515
|
+
}
|
|
1516
|
+
], { transaction: true, label: "warranty_claims.claim.escalate" });
|
|
1517
|
+
const escalatedClaim = claim;
|
|
1518
|
+
if (!escalatedClaim) {
|
|
1519
|
+
throw new CrudHttpError(404, { error: "warranty_claims.errors.notFound" });
|
|
1520
|
+
}
|
|
1521
|
+
if (!escalated) {
|
|
1522
|
+
return { claimId: escalatedClaim.id, escalationLevel: escalatedClaim.escalationLevel ?? 0, escalated: false };
|
|
1523
|
+
}
|
|
1524
|
+
await emitClaimCrud(ctx, "updated", escalatedClaim);
|
|
1525
|
+
if (input.reassignToUserId) {
|
|
1526
|
+
await emitWarrantyClaimsEvent("warranty_claims.claim.assigned", {
|
|
1527
|
+
...claimEventPayload(escalatedClaim),
|
|
1528
|
+
assigneeUserId: escalatedClaim.assigneeUserId ?? null
|
|
1529
|
+
}, claimPersistentEmitOptions(escalatedClaim));
|
|
1530
|
+
}
|
|
1531
|
+
await emitWarrantyClaimsEvent("warranty_claims.claim.escalated", {
|
|
1532
|
+
...claimEventPayload(escalatedClaim),
|
|
1533
|
+
level: escalatedClaim.escalationLevel,
|
|
1534
|
+
escalationLevel: escalatedClaim.escalationLevel,
|
|
1535
|
+
reassignToUserId: input.reassignToUserId ?? null
|
|
1536
|
+
}, claimPersistentEmitOptions(escalatedClaim));
|
|
1537
|
+
return { claimId: escalatedClaim.id, escalationLevel: escalatedClaim.escalationLevel, escalated: true };
|
|
1538
|
+
}
|
|
1539
|
+
};
|
|
1540
|
+
const commentClaimCommand = {
|
|
1541
|
+
id: "warranty_claims.claim.comment",
|
|
1542
|
+
async execute(rawInput, ctx) {
|
|
1543
|
+
const input = parseCommandInput(commentClaimInputSchema, rawInput);
|
|
1544
|
+
const scope = resolveScope(ctx, {});
|
|
1545
|
+
const em = ctx.container.resolve("em").fork();
|
|
1546
|
+
const claim = await requireScopedClaim(em, input.claimId, scope);
|
|
1547
|
+
await enforceWarrantyClaimOptimisticLock(ctx, claim);
|
|
1548
|
+
let autoResumed = false;
|
|
1549
|
+
await withAtomicFlush(em, [
|
|
1550
|
+
async () => {
|
|
1551
|
+
const fromStatus = claim.status;
|
|
1552
|
+
appendClaimEvent(em, claim, "comment", {
|
|
1553
|
+
visibility: input.visibility,
|
|
1554
|
+
body: input.body,
|
|
1555
|
+
actorUserId: input.actorCustomerId ? null : ctx.auth?.sub ?? null,
|
|
1556
|
+
actorCustomerId: input.actorCustomerId ?? null
|
|
1557
|
+
});
|
|
1558
|
+
claim.awaitingStaffReply = Boolean(input.actorCustomerId);
|
|
1559
|
+
claim.updatedAt = /* @__PURE__ */ new Date();
|
|
1560
|
+
if (input.actorCustomerId && fromStatus === "info_requested") {
|
|
1561
|
+
const now = /* @__PURE__ */ new Date();
|
|
1562
|
+
const effectiveSettings = await resolveEffectiveWarrantyClaimSettings(em, scope);
|
|
1563
|
+
claim.status = "in_review";
|
|
1564
|
+
applySlaResume(em, claim, fromStatus, "in_review", now, effectiveSettings);
|
|
1565
|
+
claim.updatedAt = now;
|
|
1566
|
+
autoResumed = true;
|
|
1567
|
+
appendClaimEvent(em, claim, "status_changed", {
|
|
1568
|
+
visibility: "customer",
|
|
1569
|
+
payload: { from: fromStatus, to: "in_review" },
|
|
1570
|
+
actorUserId: null
|
|
1571
|
+
});
|
|
1572
|
+
}
|
|
1573
|
+
}
|
|
1574
|
+
], { transaction: true, label: "warranty_claims.claim.comment" });
|
|
1575
|
+
if (input.visibility === "customer") {
|
|
1576
|
+
await emitCustomerVisibleCommentAdded(ctx, claim, input);
|
|
1577
|
+
}
|
|
1578
|
+
await emitClaimCrud(ctx, "updated", claim);
|
|
1579
|
+
if (autoResumed) {
|
|
1580
|
+
await emitClaimStatusChanged(ctx, claim, "info_requested", "in_review");
|
|
1581
|
+
}
|
|
1582
|
+
return { claimId: claim.id };
|
|
1583
|
+
}
|
|
1584
|
+
};
|
|
1585
|
+
const createVendorRecoveryCommand = {
|
|
1586
|
+
id: "warranty_claims.claim.create_vendor_recovery",
|
|
1587
|
+
async execute(rawInput, ctx) {
|
|
1588
|
+
const input = parseCommandInput(vendorRecoveryInputSchema, rawInput);
|
|
1589
|
+
const scope = resolveScope(ctx, input);
|
|
1590
|
+
const em = ctx.container.resolve("em").fork();
|
|
1591
|
+
const sourceClaim = await requireScopedClaim(em, input.claimId, scope);
|
|
1592
|
+
await enforceWarrantyClaimOptimisticLock(ctx, sourceClaim);
|
|
1593
|
+
const preflightLines = await findWithDecryption(
|
|
1594
|
+
em,
|
|
1595
|
+
WarrantyClaimLine,
|
|
1596
|
+
{
|
|
1597
|
+
id: { $in: input.lineIds },
|
|
1598
|
+
claim: sourceClaim.id,
|
|
1599
|
+
tenantId: scope.tenantId,
|
|
1600
|
+
organizationId: scope.organizationId,
|
|
1601
|
+
deletedAt: null
|
|
1602
|
+
},
|
|
1603
|
+
{},
|
|
1604
|
+
scope
|
|
1605
|
+
);
|
|
1606
|
+
assertVendorRecoveryLines(preflightLines, input.lineIds);
|
|
1607
|
+
const numberGenerator = resolveClaimNumberGenerator(ctx, em);
|
|
1608
|
+
const generated = await numberGenerator.generate({
|
|
1609
|
+
claimType: "vendor_recovery",
|
|
1610
|
+
tenantId: scope.tenantId,
|
|
1611
|
+
organizationId: scope.organizationId
|
|
1612
|
+
});
|
|
1613
|
+
let recoveryClaim;
|
|
1614
|
+
let copiedLines = [];
|
|
1615
|
+
let updatedSourceLines = [];
|
|
1616
|
+
await em.transactional(async (tx) => {
|
|
1617
|
+
const lockedSource = await requireScopedClaim(tx, sourceClaim.id, scope, { lockMode: LockMode.PESSIMISTIC_WRITE });
|
|
1618
|
+
const lockedLines = await findWithDecryption(
|
|
1619
|
+
tx,
|
|
1620
|
+
WarrantyClaimLine,
|
|
1621
|
+
{
|
|
1622
|
+
id: { $in: input.lineIds },
|
|
1623
|
+
claim: lockedSource.id,
|
|
1624
|
+
tenantId: scope.tenantId,
|
|
1625
|
+
organizationId: scope.organizationId,
|
|
1626
|
+
deletedAt: null
|
|
1627
|
+
},
|
|
1628
|
+
{ lockMode: LockMode.PESSIMISTIC_WRITE },
|
|
1629
|
+
scope
|
|
1630
|
+
);
|
|
1631
|
+
assertVendorRecoveryLines(lockedLines, input.lineIds);
|
|
1632
|
+
updatedSourceLines = lockedLines;
|
|
1633
|
+
recoveryClaim = tx.create(WarrantyClaim, {
|
|
1634
|
+
id: randomUUID(),
|
|
1635
|
+
organizationId: scope.organizationId,
|
|
1636
|
+
tenantId: scope.tenantId,
|
|
1637
|
+
claimNumber: generated.number,
|
|
1638
|
+
claimType: "vendor_recovery",
|
|
1639
|
+
status: "draft",
|
|
1640
|
+
channel: "staff",
|
|
1641
|
+
priority: lockedSource.priority,
|
|
1642
|
+
customerId: lockedSource.customerId ?? null,
|
|
1643
|
+
customerName: lockedSource.customerName ?? null,
|
|
1644
|
+
vendorName: input.vendorName,
|
|
1645
|
+
vendorRef: input.vendorRef ?? null,
|
|
1646
|
+
sourceClaimId: lockedSource.id,
|
|
1647
|
+
advanceReplacement: false,
|
|
1648
|
+
currencyCode: lockedSource.currencyCode ?? null,
|
|
1649
|
+
totalClaimedAmount: "0",
|
|
1650
|
+
totalApprovedAmount: "0",
|
|
1651
|
+
totalRecoveredAmount: "0",
|
|
1652
|
+
createdAt: /* @__PURE__ */ new Date(),
|
|
1653
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
1654
|
+
});
|
|
1655
|
+
tx.persist(recoveryClaim);
|
|
1656
|
+
const lineOrder = new Map(input.lineIds.map((id, index) => [id, index]));
|
|
1657
|
+
copiedLines = [...lockedLines].sort((left, right) => (lineOrder.get(left.id) ?? 0) - (lineOrder.get(right.id) ?? 0)).map((sourceLine, index) => {
|
|
1658
|
+
const copiedLine = tx.create(WarrantyClaimLine, {
|
|
1659
|
+
id: randomUUID(),
|
|
1660
|
+
claim: recoveryClaim,
|
|
1661
|
+
organizationId: scope.organizationId,
|
|
1662
|
+
tenantId: scope.tenantId,
|
|
1663
|
+
lineNo: index + 1,
|
|
1664
|
+
productId: sourceLine.productId ?? null,
|
|
1665
|
+
variantId: sourceLine.variantId ?? null,
|
|
1666
|
+
sku: sourceLine.sku ?? null,
|
|
1667
|
+
productName: sourceLine.productName ?? null,
|
|
1668
|
+
orderLineId: sourceLine.orderLineId ?? null,
|
|
1669
|
+
serialNumber: sourceLine.serialNumber ?? null,
|
|
1670
|
+
lotNumber: sourceLine.lotNumber ?? null,
|
|
1671
|
+
purchaseDate: sourceLine.purchaseDate ?? null,
|
|
1672
|
+
warrantyMonths: sourceLine.warrantyMonths ?? null,
|
|
1673
|
+
warrantyExpiresAt: sourceLine.warrantyExpiresAt ?? null,
|
|
1674
|
+
warrantyStatus: sourceLine.warrantyStatus,
|
|
1675
|
+
faultCode: sourceLine.faultCode ?? null,
|
|
1676
|
+
faultDescription: sourceLine.faultDescription ?? null,
|
|
1677
|
+
qtyClaimed: sourceLine.qtyApproved ?? sourceLine.qtyClaimed,
|
|
1678
|
+
qtyApproved: sourceLine.qtyApproved ?? null,
|
|
1679
|
+
conditionOnReceipt: sourceLine.conditionOnReceipt ?? null,
|
|
1680
|
+
inspectionNotes: sourceLine.inspectionNotes ?? null,
|
|
1681
|
+
disposition: sourceLine.disposition ?? null,
|
|
1682
|
+
lineStatus: "pending",
|
|
1683
|
+
creditAmount: sourceLine.creditAmount ?? null,
|
|
1684
|
+
restockingFee: sourceLine.restockingFee ?? null,
|
|
1685
|
+
coreChargeAmount: sourceLine.coreChargeAmount ?? null,
|
|
1686
|
+
coreCreditAmount: sourceLine.coreCreditAmount ?? null,
|
|
1687
|
+
createdAt: /* @__PURE__ */ new Date(),
|
|
1688
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
1689
|
+
});
|
|
1690
|
+
sourceLine.vendorClaimLineId = copiedLine.id;
|
|
1691
|
+
sourceLine.updatedAt = /* @__PURE__ */ new Date();
|
|
1692
|
+
tx.persist(copiedLine);
|
|
1693
|
+
return copiedLine;
|
|
1694
|
+
});
|
|
1695
|
+
const totals = computeHeaderRollups(copiedLines);
|
|
1696
|
+
recoveryClaim.totalClaimedAmount = String(totals.totalClaimedAmount);
|
|
1697
|
+
recoveryClaim.totalApprovedAmount = String(totals.totalApprovedAmount);
|
|
1698
|
+
appendClaimEvent(tx, lockedSource, "system", {
|
|
1699
|
+
visibility: "internal",
|
|
1700
|
+
payload: { action: "vendor_recovery_created", recoveryClaimId: recoveryClaim.id },
|
|
1701
|
+
actorUserId: ctx.auth?.sub ?? null
|
|
1702
|
+
});
|
|
1703
|
+
appendClaimEvent(tx, recoveryClaim, "system", {
|
|
1704
|
+
visibility: "internal",
|
|
1705
|
+
payload: { action: "created_from_source_claim", sourceClaimId: lockedSource.id },
|
|
1706
|
+
actorUserId: ctx.auth?.sub ?? null
|
|
1707
|
+
});
|
|
1708
|
+
await tx.flush();
|
|
1709
|
+
});
|
|
1710
|
+
await emitClaimCrud(ctx, "created", recoveryClaim);
|
|
1711
|
+
await Promise.all(copiedLines.map((line) => emitLineCrud(ctx, "created", line)));
|
|
1712
|
+
await Promise.all(updatedSourceLines.map((line) => emitLineCrud(ctx, "updated", line)));
|
|
1713
|
+
await Promise.all(input.lineIds.map((sourceLineId) => invalidateCrudCache(
|
|
1714
|
+
ctx.container,
|
|
1715
|
+
"warranty_claims.claim_line",
|
|
1716
|
+
{ id: sourceLineId, organizationId: sourceClaim.organizationId, tenantId: sourceClaim.tenantId },
|
|
1717
|
+
ctx.auth?.tenantId ?? null,
|
|
1718
|
+
"warranty_claims.vendor_recovery.source_lines"
|
|
1719
|
+
)));
|
|
1720
|
+
return { claimId: recoveryClaim.id };
|
|
1721
|
+
}
|
|
1722
|
+
};
|
|
1723
|
+
const salesReturnBridgeLogger = createLogger("warranty_claims");
|
|
1724
|
+
const SALES_RETURN_ELIGIBLE_CLAIM_STATUSES = /* @__PURE__ */ new Set(["approved", "awaiting_return", "received", "inspecting"]);
|
|
1725
|
+
const SALES_RETURN_ELIGIBLE_LINE_STATUSES = /* @__PURE__ */ new Set(["approved", "received", "inspected", "resolved"]);
|
|
1726
|
+
function scrubbedDispatchContext(ctx) {
|
|
1727
|
+
return { ...ctx, request: void 0 };
|
|
1728
|
+
}
|
|
1729
|
+
function isSalesQuantityRejection(err) {
|
|
1730
|
+
if (!(err instanceof CrudHttpError) || err.status !== 400) return false;
|
|
1731
|
+
const body = err.body;
|
|
1732
|
+
const message = typeof body?.error === "string" ? body.error : "";
|
|
1733
|
+
return message.includes("quantityExceedsShipped") || message.includes("Cannot return more than the shipped quantity");
|
|
1734
|
+
}
|
|
1735
|
+
async function dispatchSalesReturnDelete(ctx, scope, salesReturnId, orderId) {
|
|
1736
|
+
const commandBus = ctx.container.resolve("commandBus");
|
|
1737
|
+
await commandBus.execute(
|
|
1738
|
+
"sales.returns.delete",
|
|
1739
|
+
{
|
|
1740
|
+
input: { id: salesReturnId, orderId, tenantId: scope.tenantId, organizationId: scope.organizationId },
|
|
1741
|
+
ctx: scrubbedDispatchContext(ctx)
|
|
1742
|
+
}
|
|
1743
|
+
);
|
|
1744
|
+
}
|
|
1745
|
+
const createSalesReturnCommand = {
|
|
1746
|
+
id: "warranty_claims.claim.create_sales_return",
|
|
1747
|
+
isUndoable: true,
|
|
1748
|
+
prepare: async (rawInput, ctx) => {
|
|
1749
|
+
const input = parseCommandInput(claimCreateSalesReturnSchema, rawInput);
|
|
1750
|
+
const scope = resolveScope(ctx, input);
|
|
1751
|
+
const em = ctx.container.resolve("em").fork();
|
|
1752
|
+
return { before: await loadClaimSnapshot(em, input.id, scope) };
|
|
1753
|
+
},
|
|
1754
|
+
async execute(rawInput, ctx) {
|
|
1755
|
+
const input = parseCommandInput(claimCreateSalesReturnSchema, rawInput);
|
|
1756
|
+
const scope = resolveScope(ctx, input);
|
|
1757
|
+
const em = ctx.container.resolve("em").fork();
|
|
1758
|
+
const claim = await requireScopedClaim(em, input.id, scope);
|
|
1759
|
+
await enforceWarrantyClaimOptimisticLock(ctx, claim, WARRANTY_CLAIM_RESOURCE_KIND, input.updatedAt ?? void 0);
|
|
1760
|
+
if (!claim.orderId) {
|
|
1761
|
+
throw new CrudHttpError(400, { error: "warranty_claims.errors.salesReturnRequiresOrder" });
|
|
1762
|
+
}
|
|
1763
|
+
if (!SALES_RETURN_ELIGIBLE_CLAIM_STATUSES.has(claim.status)) {
|
|
1764
|
+
throw new CrudHttpError(400, { error: "warranty_claims.errors.salesReturnStatusNotEligible" });
|
|
1765
|
+
}
|
|
1766
|
+
if (claim.salesReturnId) {
|
|
1767
|
+
throw new CrudHttpError(400, { error: "warranty_claims.errors.salesReturnAlreadyLinked" });
|
|
1768
|
+
}
|
|
1769
|
+
if (!resolveOptionalEntityId("sales", "sales_return")) {
|
|
1770
|
+
throw new CrudHttpError(400, { error: "warranty_claims.errors.salesUnavailable" });
|
|
1771
|
+
}
|
|
1772
|
+
const lines = await findWithDecryption(
|
|
1773
|
+
em,
|
|
1774
|
+
WarrantyClaimLine,
|
|
1775
|
+
{ claim: claim.id, tenantId: scope.tenantId, organizationId: scope.organizationId, deletedAt: null },
|
|
1776
|
+
{},
|
|
1777
|
+
scope
|
|
1778
|
+
);
|
|
1779
|
+
const eligibleLines = [];
|
|
1780
|
+
const skippedLineIds = [];
|
|
1781
|
+
for (const line of lines) {
|
|
1782
|
+
const effectiveQty = line.qtyApproved ?? line.qtyClaimed;
|
|
1783
|
+
const quantityUnits = claimedQuantityUnits(effectiveQty) ?? 0n;
|
|
1784
|
+
const eligible = Boolean(line.orderLineId) && SALES_RETURN_ELIGIBLE_LINE_STATUSES.has(line.lineStatus) && quantityUnits > 0n && quantityUnits % CLAIMED_QUANTITY_SCALE === 0n;
|
|
1785
|
+
if (eligible && line.orderLineId) {
|
|
1786
|
+
eligibleLines.push({ orderLineId: line.orderLineId, quantity: String(effectiveQty) });
|
|
1787
|
+
} else {
|
|
1788
|
+
skippedLineIds.push(line.id);
|
|
1789
|
+
}
|
|
1790
|
+
}
|
|
1791
|
+
if (eligibleLines.length === 0) {
|
|
1792
|
+
throw new CrudHttpError(400, { error: "warranty_claims.errors.salesReturnNoEligibleLines" });
|
|
1793
|
+
}
|
|
1794
|
+
const commandBus = ctx.container.resolve("commandBus");
|
|
1795
|
+
let salesReturnId;
|
|
1796
|
+
try {
|
|
1797
|
+
const { result: dispatchedResult } = await commandBus.execute(
|
|
1798
|
+
"sales.returns.create",
|
|
1799
|
+
{
|
|
1800
|
+
input: {
|
|
1801
|
+
orderId: claim.orderId,
|
|
1802
|
+
reason: claim.claimNumber,
|
|
1803
|
+
tenantId: scope.tenantId,
|
|
1804
|
+
organizationId: scope.organizationId,
|
|
1805
|
+
lines: eligibleLines
|
|
1806
|
+
},
|
|
1807
|
+
ctx: scrubbedDispatchContext(ctx)
|
|
1808
|
+
}
|
|
1809
|
+
);
|
|
1810
|
+
if (!dispatchedResult?.returnId) {
|
|
1811
|
+
throw new CrudHttpError(400, { error: "warranty_claims.errors.save_failed" });
|
|
1812
|
+
}
|
|
1813
|
+
salesReturnId = dispatchedResult.returnId;
|
|
1814
|
+
} catch (err) {
|
|
1815
|
+
if (isSalesQuantityRejection(err)) {
|
|
1816
|
+
throw new CrudHttpError(400, { error: "warranty_claims.errors.salesReturnQuantityRejected" });
|
|
1817
|
+
}
|
|
1818
|
+
throw err;
|
|
1819
|
+
}
|
|
1820
|
+
let salesReturnUpdatedAt = null;
|
|
1821
|
+
try {
|
|
1822
|
+
const createdReturnRow = await querySalesReferenceRow(ctx, "sales_returns", scope, salesReturnId, ["id", "updated_at"]);
|
|
1823
|
+
salesReturnUpdatedAt = createdReturnRow?.updated_at ? new Date(createdReturnRow.updated_at).toISOString() : null;
|
|
1824
|
+
await em.transactional(async (tx) => {
|
|
1825
|
+
const lockedClaim = await requireScopedClaim(tx, claim.id, scope, { lockMode: LockMode.PESSIMISTIC_WRITE });
|
|
1826
|
+
if (lockedClaim.salesReturnId) {
|
|
1827
|
+
throw new CrudHttpError(400, { error: "warranty_claims.errors.salesReturnAlreadyLinked" });
|
|
1828
|
+
}
|
|
1829
|
+
lockedClaim.salesReturnId = salesReturnId;
|
|
1830
|
+
lockedClaim.updatedAt = /* @__PURE__ */ new Date();
|
|
1831
|
+
appendClaimEvent(tx, lockedClaim, "system", {
|
|
1832
|
+
visibility: "internal",
|
|
1833
|
+
payload: { action: "sales_return_created", salesReturnId },
|
|
1834
|
+
actorUserId: ctx.auth?.sub ?? null
|
|
1835
|
+
});
|
|
1836
|
+
claim.salesReturnId = salesReturnId;
|
|
1837
|
+
claim.updatedAt = lockedClaim.updatedAt;
|
|
1838
|
+
await tx.flush();
|
|
1839
|
+
});
|
|
1840
|
+
} catch (err) {
|
|
1841
|
+
try {
|
|
1842
|
+
await dispatchSalesReturnDelete(ctx, scope, salesReturnId, claim.orderId);
|
|
1843
|
+
} catch (compensationErr) {
|
|
1844
|
+
salesReturnBridgeLogger.error("warranty_claims.claim.create_sales_return compensation failed \u2014 orphaned sales return", {
|
|
1845
|
+
err: compensationErr,
|
|
1846
|
+
claimId: claim.id,
|
|
1847
|
+
salesReturnId
|
|
1848
|
+
});
|
|
1849
|
+
const orphanEm = ctx.container.resolve("em").fork();
|
|
1850
|
+
const orphanClaim = await findOneWithDecryption(
|
|
1851
|
+
orphanEm,
|
|
1852
|
+
WarrantyClaim,
|
|
1853
|
+
{ id: claim.id, tenantId: scope.tenantId, organizationId: scope.organizationId, deletedAt: null },
|
|
1854
|
+
{},
|
|
1855
|
+
scope
|
|
1856
|
+
);
|
|
1857
|
+
if (orphanClaim) {
|
|
1858
|
+
try {
|
|
1859
|
+
appendClaimEvent(orphanEm, orphanClaim, "system", {
|
|
1860
|
+
visibility: "internal",
|
|
1861
|
+
payload: { action: "sales_return_orphaned", salesReturnId },
|
|
1862
|
+
actorUserId: ctx.auth?.sub ?? null
|
|
1863
|
+
});
|
|
1864
|
+
await orphanEm.flush();
|
|
1865
|
+
} catch (orphanEventErr) {
|
|
1866
|
+
salesReturnBridgeLogger.error("warranty_claims.claim.create_sales_return orphan timeline write failed", {
|
|
1867
|
+
err: orphanEventErr,
|
|
1868
|
+
claimId: claim.id,
|
|
1869
|
+
salesReturnId
|
|
1870
|
+
});
|
|
1871
|
+
}
|
|
1872
|
+
}
|
|
1873
|
+
throw new CrudHttpError(500, { error: "warranty_claims.errors.save_failed" });
|
|
1874
|
+
}
|
|
1875
|
+
throw err;
|
|
1876
|
+
}
|
|
1877
|
+
await emitClaimCrud(ctx, "updated", claim);
|
|
1878
|
+
return { claimId: claim.id, salesReturnId, salesReturnUpdatedAt, skippedLineIds };
|
|
1879
|
+
},
|
|
1880
|
+
captureAfter: async (rawInput, result, ctx) => {
|
|
1881
|
+
const input = parseCommandInput(claimCreateSalesReturnSchema, rawInput);
|
|
1882
|
+
const scope = resolveScope(ctx, input);
|
|
1883
|
+
const em = ctx.container.resolve("em").fork();
|
|
1884
|
+
return loadClaimSnapshot(em, result.claimId, scope);
|
|
1885
|
+
},
|
|
1886
|
+
buildLog: async ({ result, snapshots }) => {
|
|
1887
|
+
const base = buildClaimLog("warranty_claims.audit.claim.create_sales_return", result.claimId, snapshots);
|
|
1888
|
+
const after = snapshots.after;
|
|
1889
|
+
const undoPayload = {
|
|
1890
|
+
...base.payload.undo,
|
|
1891
|
+
salesReturn: {
|
|
1892
|
+
id: result.salesReturnId,
|
|
1893
|
+
orderId: after?.orderId ?? "",
|
|
1894
|
+
updatedAt: result.salesReturnUpdatedAt
|
|
1895
|
+
}
|
|
1896
|
+
};
|
|
1897
|
+
return { ...base, payload: { undo: undoPayload } };
|
|
1898
|
+
},
|
|
1899
|
+
undo: async ({ logEntry, ctx }) => {
|
|
1900
|
+
const payload = extractUndoPayload(logEntry);
|
|
1901
|
+
const before = payload?.before;
|
|
1902
|
+
if (!before) return;
|
|
1903
|
+
const after = payload?.after;
|
|
1904
|
+
if (!after) throwClaimUndoConflict();
|
|
1905
|
+
await assertUndoSnapshotCurrent(ctx, after);
|
|
1906
|
+
const scope = { tenantId: before.tenantId, organizationId: before.organizationId };
|
|
1907
|
+
const salesReturn = payload?.salesReturn ?? null;
|
|
1908
|
+
if (salesReturn?.id && salesReturn.orderId) {
|
|
1909
|
+
const currentRow = await querySalesReferenceRow(ctx, "sales_returns", scope, salesReturn.id, ["id", "updated_at"]);
|
|
1910
|
+
if (currentRow) {
|
|
1911
|
+
const currentUpdatedAt = currentRow.updated_at ? new Date(currentRow.updated_at).toISOString() : null;
|
|
1912
|
+
if (!salesReturn.updatedAt || currentUpdatedAt && currentUpdatedAt !== salesReturn.updatedAt) {
|
|
1913
|
+
throw new CrudHttpError(409, { error: "warranty_claims.errors.salesReturnChangedUndoAborted" });
|
|
1914
|
+
}
|
|
1915
|
+
await dispatchSalesReturnDelete(ctx, scope, salesReturn.id, salesReturn.orderId);
|
|
1916
|
+
}
|
|
1917
|
+
}
|
|
1918
|
+
const em = ctx.container.resolve("em").fork();
|
|
1919
|
+
let claim;
|
|
1920
|
+
await withAtomicFlush(em, [
|
|
1921
|
+
async () => {
|
|
1922
|
+
claim = await restoreSnapshot(em, before, after);
|
|
1923
|
+
appendClaimEvent(em, claim, "system", {
|
|
1924
|
+
visibility: "internal",
|
|
1925
|
+
payload: { action: "undo_sales_return_created" },
|
|
1926
|
+
actorUserId: ctx.auth?.sub ?? null
|
|
1927
|
+
});
|
|
1928
|
+
}
|
|
1929
|
+
], { transaction: true, label: "warranty_claims.claim.create_sales_return.undo" });
|
|
1930
|
+
await emitClaimUndoCrud(ctx, "updated", claim);
|
|
1931
|
+
}
|
|
1932
|
+
};
|
|
1933
|
+
const creditMemoBridgeLogger = createLogger("warranty_claims");
|
|
1934
|
+
const CREDIT_MEMO_ELIGIBLE_CLAIM_STATUSES = /* @__PURE__ */ new Set(["received", "inspecting", "resolved"]);
|
|
1935
|
+
const CREDIT_MEMO_ELIGIBLE_LINE_STATUSES = /* @__PURE__ */ new Set(["received", "inspected", "resolved"]);
|
|
1936
|
+
const CREDIT_MEMO_ELIGIBLE_DISPOSITIONS = /* @__PURE__ */ new Set(["credit", "refund"]);
|
|
1937
|
+
function minimumUnits(left, right) {
|
|
1938
|
+
return left < right ? left : right;
|
|
1939
|
+
}
|
|
1940
|
+
function prepareCreditMemoLine(claimLine, sourceLine, creditedQuantityUnits, currencyCode) {
|
|
1941
|
+
const orderQuantityUnits = claimedQuantityUnits(sourceLine.quantity);
|
|
1942
|
+
const sourceGrossUnits = claimedQuantityUnits(sourceLine.total_gross_amount);
|
|
1943
|
+
const sourceNetUnits = claimedQuantityUnits(sourceLine.total_net_amount);
|
|
1944
|
+
const taxRateUnits = claimedQuantityUnits(sourceLine.tax_rate);
|
|
1945
|
+
if (orderQuantityUnits === null || orderQuantityUnits <= 0n || creditedQuantityUnits > orderQuantityUnits || sourceGrossUnits === null || sourceNetUnits === null || taxRateUnits === null) {
|
|
1946
|
+
return null;
|
|
1947
|
+
}
|
|
1948
|
+
let netBasisUnits = sourceNetUnits;
|
|
1949
|
+
if (sourceGrossUnits > 0n && netBasisUnits <= 0n) {
|
|
1950
|
+
netBasisUnits = netUnitsFromGross(sourceGrossUnits, taxRateUnits);
|
|
1951
|
+
}
|
|
1952
|
+
const proratedGrossUnits = roundHalfUpDivision(sourceGrossUnits * creditedQuantityUnits, orderQuantityUnits);
|
|
1953
|
+
const proratedNetUnits = roundHalfUpDivision(netBasisUnits * creditedQuantityUnits, orderQuantityUnits);
|
|
1954
|
+
const creditAmountUnits = claimedQuantityUnits(claimLine.creditAmount);
|
|
1955
|
+
const restockingFeeUnits = claimedQuantityUnits(claimLine.restockingFee) ?? 0n;
|
|
1956
|
+
const coreCreditUnits = claimedQuantityUnits(claimLine.coreCreditAmount) ?? 0n;
|
|
1957
|
+
const baseGrossUnits = creditAmountUnits ?? proratedGrossUnits;
|
|
1958
|
+
const adjustedGrossUnits = baseGrossUnits - restockingFeeUnits + coreCreditUnits;
|
|
1959
|
+
const grossUnits = adjustedGrossUnits > 0n ? adjustedGrossUnits : 0n;
|
|
1960
|
+
let netUnits = grossUnits === proratedGrossUnits ? proratedNetUnits : netUnitsFromGross(grossUnits, taxRateUnits);
|
|
1961
|
+
if (grossUnits > 0n && netUnits <= 0n) {
|
|
1962
|
+
netUnits = netUnitsFromGross(grossUnits, taxRateUnits);
|
|
1963
|
+
}
|
|
1964
|
+
const taxUnits = grossUnits - netUnits;
|
|
1965
|
+
const input = {
|
|
1966
|
+
orderLineId: claimLine.orderLineId,
|
|
1967
|
+
quantity: formatScaledUnits(creditedQuantityUnits),
|
|
1968
|
+
currencyCode,
|
|
1969
|
+
unitPriceNet: formatScaledUnits(roundHalfUpDivision(netUnits * CLAIMED_QUANTITY_SCALE, creditedQuantityUnits)),
|
|
1970
|
+
unitPriceGross: formatScaledUnits(roundHalfUpDivision(grossUnits * CLAIMED_QUANTITY_SCALE, creditedQuantityUnits)),
|
|
1971
|
+
taxRate: formatScaledUnits(taxRateUnits),
|
|
1972
|
+
taxAmount: formatScaledUnits(taxUnits),
|
|
1973
|
+
totalNetAmount: formatScaledUnits(netUnits),
|
|
1974
|
+
totalGrossAmount: formatScaledUnits(grossUnits),
|
|
1975
|
+
metadata: { warrantyClaimLineId: claimLine.id }
|
|
1976
|
+
};
|
|
1977
|
+
const name = readString(sourceLine, "name");
|
|
1978
|
+
if (name) input.name = name;
|
|
1979
|
+
return { input, netUnits, grossUnits, taxUnits };
|
|
1980
|
+
}
|
|
1981
|
+
async function dispatchCreditMemoDelete(ctx, scope, creditMemoId) {
|
|
1982
|
+
const commandBus = ctx.container.resolve("commandBus");
|
|
1983
|
+
await commandBus.execute(
|
|
1984
|
+
"sales.credit_memos.delete",
|
|
1985
|
+
{
|
|
1986
|
+
input: { id: creditMemoId, organizationId: scope.organizationId, tenantId: scope.tenantId },
|
|
1987
|
+
ctx: scrubbedDispatchContext(ctx)
|
|
1988
|
+
}
|
|
1989
|
+
);
|
|
1990
|
+
}
|
|
1991
|
+
const createCreditMemoCommand = {
|
|
1992
|
+
id: "warranty_claims.claim.create_credit_memo",
|
|
1993
|
+
isUndoable: true,
|
|
1994
|
+
prepare: async (rawInput, ctx) => {
|
|
1995
|
+
const input = parseCommandInput(claimCreateCreditMemoSchema, rawInput);
|
|
1996
|
+
const scope = resolveScope(ctx, input);
|
|
1997
|
+
const em = ctx.container.resolve("em").fork();
|
|
1998
|
+
return { before: await loadClaimSnapshot(em, input.id, scope) };
|
|
1999
|
+
},
|
|
2000
|
+
async execute(rawInput, ctx) {
|
|
2001
|
+
const input = parseCommandInput(claimCreateCreditMemoSchema, rawInput);
|
|
2002
|
+
const scope = resolveScope(ctx, input);
|
|
2003
|
+
const em = ctx.container.resolve("em").fork();
|
|
2004
|
+
const claim = await requireScopedClaim(em, input.id, scope);
|
|
2005
|
+
await enforceWarrantyClaimOptimisticLock(ctx, claim, WARRANTY_CLAIM_RESOURCE_KIND, input.updatedAt ?? void 0);
|
|
2006
|
+
if (!claim.orderId) {
|
|
2007
|
+
throw new CrudHttpError(400, { error: "warranty_claims.errors.creditMemoRequiresOrder" });
|
|
2008
|
+
}
|
|
2009
|
+
if (!CREDIT_MEMO_ELIGIBLE_CLAIM_STATUSES.has(claim.status)) {
|
|
2010
|
+
throw new CrudHttpError(400, { error: "warranty_claims.errors.creditMemoInvalidStatus" });
|
|
2011
|
+
}
|
|
2012
|
+
if (claim.creditMemoId) {
|
|
2013
|
+
throw new CrudHttpError(400, { error: "warranty_claims.errors.creditMemoAlreadyLinked" });
|
|
2014
|
+
}
|
|
2015
|
+
if (!resolveOptionalEntityId("sales", "sales_credit_memo")) {
|
|
2016
|
+
throw new CrudHttpError(400, { error: "warranty_claims.errors.creditMemoSalesUnavailable" });
|
|
2017
|
+
}
|
|
2018
|
+
const sourceOrder = await querySalesReferenceRow(
|
|
2019
|
+
ctx,
|
|
2020
|
+
"sales_orders",
|
|
2021
|
+
scope,
|
|
2022
|
+
claim.orderId,
|
|
2023
|
+
["id", "currency_code"]
|
|
2024
|
+
);
|
|
2025
|
+
const orderCurrencyCode = sourceOrder ? readString(sourceOrder, "currency_code") : null;
|
|
2026
|
+
if (!sourceOrder || !orderCurrencyCode) {
|
|
2027
|
+
throw new CrudHttpError(400, { error: "warranty_claims.errors.creditMemoRequiresOrder" });
|
|
2028
|
+
}
|
|
2029
|
+
const claimLines = await findWithDecryption(
|
|
2030
|
+
em,
|
|
2031
|
+
WarrantyClaimLine,
|
|
2032
|
+
{ claim: claim.id, tenantId: scope.tenantId, organizationId: scope.organizationId, deletedAt: null },
|
|
2033
|
+
{},
|
|
2034
|
+
scope
|
|
2035
|
+
);
|
|
2036
|
+
const sourceLineRows = await querySalesOrderLineReferences(
|
|
2037
|
+
ctx,
|
|
2038
|
+
scope,
|
|
2039
|
+
Array.from(new Set(claimLines.flatMap((line) => line.orderLineId ? [line.orderLineId] : []))),
|
|
2040
|
+
["id", "order_id", "name", "currency_code", "quantity", "total_net_amount", "total_gross_amount", "tax_rate"]
|
|
2041
|
+
);
|
|
2042
|
+
const sourceLinesById = new Map((sourceLineRows ?? []).map((row) => [readString(row, "id"), row]));
|
|
2043
|
+
const preparedLines = [];
|
|
2044
|
+
const skippedLineIds = [];
|
|
2045
|
+
for (const claimLine of claimLines) {
|
|
2046
|
+
const approvedQuantityUnits = claimedQuantityUnits(claimLine.qtyApproved ?? claimLine.qtyClaimed) ?? 0n;
|
|
2047
|
+
const receivedQuantityUnits = claimedQuantityUnits(claimLine.qtyReceived) ?? 0n;
|
|
2048
|
+
const creditedQuantityUnits = minimumUnits(approvedQuantityUnits, receivedQuantityUnits);
|
|
2049
|
+
const initiallyEligible = Boolean(claimLine.orderLineId) && CREDIT_MEMO_ELIGIBLE_DISPOSITIONS.has(claimLine.disposition) && CREDIT_MEMO_ELIGIBLE_LINE_STATUSES.has(claimLine.lineStatus) && creditedQuantityUnits > 0n;
|
|
2050
|
+
if (!initiallyEligible || !claimLine.orderLineId) {
|
|
2051
|
+
skippedLineIds.push(claimLine.id);
|
|
2052
|
+
continue;
|
|
2053
|
+
}
|
|
2054
|
+
const sourceLine = sourceLinesById.get(claimLine.orderLineId) ?? null;
|
|
2055
|
+
const sourceCurrencyCode = sourceLine ? readString(sourceLine, "currency_code") : null;
|
|
2056
|
+
if (!sourceLine || sourceLine.order_id !== claim.orderId || sourceCurrencyCode !== orderCurrencyCode) {
|
|
2057
|
+
skippedLineIds.push(claimLine.id);
|
|
2058
|
+
continue;
|
|
2059
|
+
}
|
|
2060
|
+
const preparedLine = prepareCreditMemoLine(
|
|
2061
|
+
claimLine,
|
|
2062
|
+
sourceLine,
|
|
2063
|
+
creditedQuantityUnits,
|
|
2064
|
+
orderCurrencyCode
|
|
2065
|
+
);
|
|
2066
|
+
if (!preparedLine) {
|
|
2067
|
+
skippedLineIds.push(claimLine.id);
|
|
2068
|
+
continue;
|
|
2069
|
+
}
|
|
2070
|
+
preparedLines.push(preparedLine);
|
|
2071
|
+
}
|
|
2072
|
+
if (preparedLines.length === 0) {
|
|
2073
|
+
throw new CrudHttpError(400, { error: "warranty_claims.errors.creditMemoNoEligibleLines" });
|
|
2074
|
+
}
|
|
2075
|
+
const totals = preparedLines.reduce(
|
|
2076
|
+
(result, line) => ({
|
|
2077
|
+
netUnits: result.netUnits + line.netUnits,
|
|
2078
|
+
grossUnits: result.grossUnits + line.grossUnits,
|
|
2079
|
+
taxUnits: result.taxUnits + line.taxUnits
|
|
2080
|
+
}),
|
|
2081
|
+
{ netUnits: 0n, grossUnits: 0n, taxUnits: 0n }
|
|
2082
|
+
);
|
|
2083
|
+
const grandTotalNetAmount = formatScaledUnits(totals.netUnits);
|
|
2084
|
+
const grandTotalGrossAmount = formatScaledUnits(totals.grossUnits);
|
|
2085
|
+
const creditMemoInput = {
|
|
2086
|
+
organizationId: scope.organizationId,
|
|
2087
|
+
tenantId: scope.tenantId,
|
|
2088
|
+
orderId: claim.orderId,
|
|
2089
|
+
currencyCode: orderCurrencyCode,
|
|
2090
|
+
reason: claim.claimNumber,
|
|
2091
|
+
metadata: { warrantyClaimId: claim.id, warrantyClaimNumber: claim.claimNumber },
|
|
2092
|
+
lines: preparedLines.map((line) => line.input),
|
|
2093
|
+
subtotalNetAmount: grandTotalNetAmount,
|
|
2094
|
+
subtotalGrossAmount: grandTotalGrossAmount,
|
|
2095
|
+
taxTotalAmount: formatScaledUnits(totals.taxUnits),
|
|
2096
|
+
grandTotalNetAmount,
|
|
2097
|
+
grandTotalGrossAmount
|
|
2098
|
+
};
|
|
2099
|
+
const commandBus = ctx.container.resolve("commandBus");
|
|
2100
|
+
let creditMemoId = null;
|
|
2101
|
+
let creditMemoUpdatedAt = null;
|
|
2102
|
+
try {
|
|
2103
|
+
const { result: dispatchedResult } = await commandBus.execute(
|
|
2104
|
+
"sales.credit_memos.create",
|
|
2105
|
+
{ input: creditMemoInput, ctx: scrubbedDispatchContext(ctx) }
|
|
2106
|
+
);
|
|
2107
|
+
if (!dispatchedResult?.creditMemoId) {
|
|
2108
|
+
throw new CrudHttpError(400, { error: "warranty_claims.errors.save_failed" });
|
|
2109
|
+
}
|
|
2110
|
+
creditMemoId = dispatchedResult.creditMemoId;
|
|
2111
|
+
const createdCreditMemoRow = await querySalesReferenceRow(
|
|
2112
|
+
ctx,
|
|
2113
|
+
"sales_credit_memos",
|
|
2114
|
+
scope,
|
|
2115
|
+
creditMemoId,
|
|
2116
|
+
["id", "updated_at"]
|
|
2117
|
+
);
|
|
2118
|
+
creditMemoUpdatedAt = createdCreditMemoRow?.updated_at ? new Date(createdCreditMemoRow.updated_at).toISOString() : null;
|
|
2119
|
+
await em.transactional(async (tx) => {
|
|
2120
|
+
const lockedClaim = await requireScopedClaim(tx, claim.id, scope, { lockMode: LockMode.PESSIMISTIC_WRITE });
|
|
2121
|
+
if (lockedClaim.creditMemoId) {
|
|
2122
|
+
throw new CrudHttpError(400, { error: "warranty_claims.errors.creditMemoAlreadyLinked" });
|
|
2123
|
+
}
|
|
2124
|
+
lockedClaim.creditMemoId = creditMemoId;
|
|
2125
|
+
lockedClaim.updatedAt = /* @__PURE__ */ new Date();
|
|
2126
|
+
appendClaimEvent(tx, lockedClaim, "system", {
|
|
2127
|
+
visibility: "internal",
|
|
2128
|
+
payload: { action: "credit_memo_created", creditMemoId, grandTotalGrossAmount, currencyCode: orderCurrencyCode },
|
|
2129
|
+
actorUserId: ctx.auth?.sub ?? null
|
|
2130
|
+
});
|
|
2131
|
+
claim.creditMemoId = creditMemoId;
|
|
2132
|
+
claim.updatedAt = lockedClaim.updatedAt;
|
|
2133
|
+
await tx.flush();
|
|
2134
|
+
});
|
|
2135
|
+
} catch (err) {
|
|
2136
|
+
if (!creditMemoId) throw err;
|
|
2137
|
+
try {
|
|
2138
|
+
await dispatchCreditMemoDelete(ctx, scope, creditMemoId);
|
|
2139
|
+
} catch (compensationErr) {
|
|
2140
|
+
creditMemoBridgeLogger.error("warranty_claims.claim.create_credit_memo compensation failed \u2014 orphaned credit memo", {
|
|
2141
|
+
err: compensationErr,
|
|
2142
|
+
claimId: claim.id,
|
|
2143
|
+
creditMemoId
|
|
2144
|
+
});
|
|
2145
|
+
try {
|
|
2146
|
+
const orphanEm = ctx.container.resolve("em").fork();
|
|
2147
|
+
const orphanClaim = await findOneWithDecryption(
|
|
2148
|
+
orphanEm,
|
|
2149
|
+
WarrantyClaim,
|
|
2150
|
+
{ id: claim.id, tenantId: scope.tenantId, organizationId: scope.organizationId, deletedAt: null },
|
|
2151
|
+
{},
|
|
2152
|
+
scope
|
|
2153
|
+
);
|
|
2154
|
+
if (orphanClaim) {
|
|
2155
|
+
appendClaimEvent(orphanEm, orphanClaim, "system", {
|
|
2156
|
+
visibility: "internal",
|
|
2157
|
+
payload: { action: "credit_memo_orphaned", creditMemoId },
|
|
2158
|
+
actorUserId: ctx.auth?.sub ?? null
|
|
2159
|
+
});
|
|
2160
|
+
await orphanEm.flush();
|
|
2161
|
+
}
|
|
2162
|
+
} catch (orphanEventErr) {
|
|
2163
|
+
creditMemoBridgeLogger.error("warranty_claims.claim.create_credit_memo orphan timeline write failed", {
|
|
2164
|
+
err: orphanEventErr,
|
|
2165
|
+
claimId: claim.id,
|
|
2166
|
+
creditMemoId
|
|
2167
|
+
});
|
|
2168
|
+
}
|
|
2169
|
+
throw new CrudHttpError(500, { error: "warranty_claims.errors.save_failed" });
|
|
2170
|
+
}
|
|
2171
|
+
throw err;
|
|
2172
|
+
}
|
|
2173
|
+
await emitClaimCrud(ctx, "updated", claim);
|
|
2174
|
+
return {
|
|
2175
|
+
claimId: claim.id,
|
|
2176
|
+
creditMemoId,
|
|
2177
|
+
creditMemoUpdatedAt,
|
|
2178
|
+
skippedLineIds,
|
|
2179
|
+
grandTotalGrossAmount,
|
|
2180
|
+
currencyCode: orderCurrencyCode
|
|
2181
|
+
};
|
|
2182
|
+
},
|
|
2183
|
+
captureAfter: async (rawInput, result, ctx) => {
|
|
2184
|
+
const input = parseCommandInput(claimCreateCreditMemoSchema, rawInput);
|
|
2185
|
+
const scope = resolveScope(ctx, input);
|
|
2186
|
+
const em = ctx.container.resolve("em").fork();
|
|
2187
|
+
return loadClaimSnapshot(em, result.claimId, scope);
|
|
2188
|
+
},
|
|
2189
|
+
buildLog: async ({ result, snapshots }) => {
|
|
2190
|
+
const base = buildClaimLog("warranty_claims.audit.claim.create_credit_memo", result.claimId, snapshots);
|
|
2191
|
+
const undoPayload = {
|
|
2192
|
+
...base.payload.undo,
|
|
2193
|
+
creditMemo: { id: result.creditMemoId, updatedAt: result.creditMemoUpdatedAt }
|
|
2194
|
+
};
|
|
2195
|
+
return { ...base, payload: { undo: undoPayload } };
|
|
2196
|
+
},
|
|
2197
|
+
undo: async ({ logEntry, ctx }) => {
|
|
2198
|
+
const payload = extractUndoPayload(logEntry);
|
|
2199
|
+
const before = payload?.before;
|
|
2200
|
+
if (!before) return;
|
|
2201
|
+
const after = payload?.after;
|
|
2202
|
+
if (!after) throwClaimUndoConflict();
|
|
2203
|
+
await assertUndoSnapshotCurrent(ctx, after);
|
|
2204
|
+
const scope = { tenantId: before.tenantId, organizationId: before.organizationId };
|
|
2205
|
+
const creditMemo = payload?.creditMemo ?? null;
|
|
2206
|
+
if (creditMemo?.id) {
|
|
2207
|
+
const currentRow = await querySalesReferenceRow(ctx, "sales_credit_memos", scope, creditMemo.id, ["id", "updated_at"]);
|
|
2208
|
+
if (currentRow === void 0) {
|
|
2209
|
+
throw new CrudHttpError(409, { error: "warranty_claims.errors.creditMemoChangedUndoAborted" });
|
|
2210
|
+
}
|
|
2211
|
+
if (currentRow) {
|
|
2212
|
+
const currentUpdatedAt = currentRow.updated_at ? new Date(currentRow.updated_at).toISOString() : null;
|
|
2213
|
+
if (!creditMemo.updatedAt || !currentUpdatedAt || currentUpdatedAt !== creditMemo.updatedAt) {
|
|
2214
|
+
throw new CrudHttpError(409, { error: "warranty_claims.errors.creditMemoChangedUndoAborted" });
|
|
2215
|
+
}
|
|
2216
|
+
await dispatchCreditMemoDelete(ctx, scope, creditMemo.id);
|
|
2217
|
+
} else {
|
|
2218
|
+
creditMemoBridgeLogger.info("warranty_claims.claim.create_credit_memo undo found credit memo already absent", {
|
|
2219
|
+
claimId: before.id,
|
|
2220
|
+
creditMemoId: creditMemo.id
|
|
2221
|
+
});
|
|
2222
|
+
}
|
|
2223
|
+
}
|
|
2224
|
+
const em = ctx.container.resolve("em").fork();
|
|
2225
|
+
let claim;
|
|
2226
|
+
await withAtomicFlush(em, [
|
|
2227
|
+
async () => {
|
|
2228
|
+
claim = await restoreSnapshot(em, before, after);
|
|
2229
|
+
appendClaimEvent(em, claim, "system", {
|
|
2230
|
+
visibility: "internal",
|
|
2231
|
+
payload: { action: "undo_credit_memo_created" },
|
|
2232
|
+
actorUserId: ctx.auth?.sub ?? null
|
|
2233
|
+
});
|
|
2234
|
+
}
|
|
2235
|
+
], { transaction: true, label: "warranty_claims.claim.create_credit_memo.undo" });
|
|
2236
|
+
await emitClaimUndoCrud(ctx, "updated", claim);
|
|
2237
|
+
}
|
|
2238
|
+
};
|
|
2239
|
+
const replacementOrderBridgeLogger = createLogger("warranty_claims");
|
|
2240
|
+
const REPLACEMENT_ELIGIBLE_CLAIM_STATUSES = /* @__PURE__ */ new Set([
|
|
2241
|
+
"approved",
|
|
2242
|
+
"awaiting_return",
|
|
2243
|
+
"received",
|
|
2244
|
+
"inspecting",
|
|
2245
|
+
"resolved"
|
|
2246
|
+
]);
|
|
2247
|
+
const REPLACEMENT_ELIGIBLE_LINE_STATUSES = /* @__PURE__ */ new Set(["approved", "received", "inspected", "resolved"]);
|
|
2248
|
+
const ADVANCE_REPLACEMENT_CLAIM_STATUSES = /* @__PURE__ */ new Set(["approved", "awaiting_return"]);
|
|
2249
|
+
const SALES_ORDER_LINE_KINDS = /* @__PURE__ */ new Set(["product", "service", "shipping", "discount", "adjustment"]);
|
|
2250
|
+
function readSalesOrderLineKind(value) {
|
|
2251
|
+
return typeof value === "string" && SALES_ORDER_LINE_KINDS.has(value) ? value : null;
|
|
2252
|
+
}
|
|
2253
|
+
async function dispatchSalesOrderDelete(ctx, scope, replacementOrderId) {
|
|
2254
|
+
const commandBus = ctx.container.resolve("commandBus");
|
|
2255
|
+
await commandBus.execute(
|
|
2256
|
+
"sales.orders.delete",
|
|
2257
|
+
{
|
|
2258
|
+
input: { id: replacementOrderId, tenantId: scope.tenantId, organizationId: scope.organizationId },
|
|
2259
|
+
ctx: scrubbedDispatchContext(ctx)
|
|
2260
|
+
}
|
|
2261
|
+
);
|
|
2262
|
+
}
|
|
2263
|
+
const createReplacementOrderCommand = {
|
|
2264
|
+
id: "warranty_claims.claim.create_replacement_order",
|
|
2265
|
+
isUndoable: true,
|
|
2266
|
+
prepare: async (rawInput, ctx) => {
|
|
2267
|
+
const input = parseCommandInput(claimCreateReplacementOrderSchema, rawInput);
|
|
2268
|
+
const scope = resolveScope(ctx, input);
|
|
2269
|
+
const em = ctx.container.resolve("em").fork();
|
|
2270
|
+
return { before: await loadClaimSnapshot(em, input.id, scope) };
|
|
2271
|
+
},
|
|
2272
|
+
async execute(rawInput, ctx) {
|
|
2273
|
+
const input = parseCommandInput(claimCreateReplacementOrderSchema, rawInput);
|
|
2274
|
+
const scope = resolveScope(ctx, input);
|
|
2275
|
+
const em = ctx.container.resolve("em").fork();
|
|
2276
|
+
const claim = await requireScopedClaim(em, input.id, scope);
|
|
2277
|
+
await enforceWarrantyClaimOptimisticLock(ctx, claim, WARRANTY_CLAIM_RESOURCE_KIND, input.updatedAt ?? void 0);
|
|
2278
|
+
if (!claim.orderId) {
|
|
2279
|
+
throw new CrudHttpError(400, { error: "warranty_claims.errors.replacementOrderRequiresOrder" });
|
|
2280
|
+
}
|
|
2281
|
+
if (!REPLACEMENT_ELIGIBLE_CLAIM_STATUSES.has(claim.status)) {
|
|
2282
|
+
throw new CrudHttpError(400, { error: "warranty_claims.errors.replacementInvalidStatus" });
|
|
2283
|
+
}
|
|
2284
|
+
if (claim.replacementOrderId) {
|
|
2285
|
+
throw new CrudHttpError(400, { error: "warranty_claims.errors.replacementAlreadyLinked" });
|
|
2286
|
+
}
|
|
2287
|
+
if (!resolveOptionalEntityId("sales", "sales_order")) {
|
|
2288
|
+
throw new CrudHttpError(400, { error: "warranty_claims.errors.replacementSalesUnavailable" });
|
|
2289
|
+
}
|
|
2290
|
+
const sourceOrder = await querySalesReferenceRow(
|
|
2291
|
+
ctx,
|
|
2292
|
+
"sales_orders",
|
|
2293
|
+
scope,
|
|
2294
|
+
claim.orderId,
|
|
2295
|
+
[
|
|
2296
|
+
"id",
|
|
2297
|
+
"currency_code",
|
|
2298
|
+
"customer_entity_id",
|
|
2299
|
+
"customer_contact_id",
|
|
2300
|
+
"billing_address_id",
|
|
2301
|
+
"shipping_address_id",
|
|
2302
|
+
"channel_id"
|
|
2303
|
+
]
|
|
2304
|
+
);
|
|
2305
|
+
const orderCurrencyCode = sourceOrder ? readString(sourceOrder, "currency_code") : null;
|
|
2306
|
+
if (!sourceOrder || !orderCurrencyCode) {
|
|
2307
|
+
throw new CrudHttpError(400, { error: "warranty_claims.errors.replacementOrderRequiresOrder" });
|
|
2308
|
+
}
|
|
2309
|
+
const lines = await findWithDecryption(
|
|
2310
|
+
em,
|
|
2311
|
+
WarrantyClaimLine,
|
|
2312
|
+
{ claim: claim.id, tenantId: scope.tenantId, organizationId: scope.organizationId, deletedAt: null },
|
|
2313
|
+
{},
|
|
2314
|
+
scope
|
|
2315
|
+
);
|
|
2316
|
+
const sourceLineRows = await querySalesOrderLineReferences(
|
|
2317
|
+
ctx,
|
|
2318
|
+
scope,
|
|
2319
|
+
Array.from(new Set(lines.flatMap((line) => line.orderLineId ? [line.orderLineId] : []))),
|
|
2320
|
+
[
|
|
2321
|
+
"id",
|
|
2322
|
+
"order_id",
|
|
2323
|
+
"product_id",
|
|
2324
|
+
"product_variant_id",
|
|
2325
|
+
"name",
|
|
2326
|
+
"kind",
|
|
2327
|
+
"currency_code",
|
|
2328
|
+
"unit_price_net",
|
|
2329
|
+
"unit_price_gross",
|
|
2330
|
+
"tax_rate"
|
|
2331
|
+
]
|
|
2332
|
+
);
|
|
2333
|
+
const sourceLinesById = new Map((sourceLineRows ?? []).map((row) => [readString(row, "id"), row]));
|
|
2334
|
+
const replacementLines = [];
|
|
2335
|
+
const skippedLineIds = [];
|
|
2336
|
+
for (const line of lines) {
|
|
2337
|
+
const effectiveQty = line.qtyApproved ?? line.qtyClaimed;
|
|
2338
|
+
const quantityUnits = claimedQuantityUnits(effectiveQty) ?? 0n;
|
|
2339
|
+
const initiallyEligible = line.disposition === "replace" && Boolean(line.orderLineId) && REPLACEMENT_ELIGIBLE_LINE_STATUSES.has(line.lineStatus) && quantityUnits > 0n && quantityUnits % CLAIMED_QUANTITY_SCALE === 0n;
|
|
2340
|
+
if (!initiallyEligible || !line.orderLineId) {
|
|
2341
|
+
skippedLineIds.push(line.id);
|
|
2342
|
+
continue;
|
|
2343
|
+
}
|
|
2344
|
+
const sourceLine = sourceLinesById.get(line.orderLineId) ?? null;
|
|
2345
|
+
const productId = sourceLine ? readString(sourceLine, "product_id") : null;
|
|
2346
|
+
const productVariantId = sourceLine ? readString(sourceLine, "product_variant_id") : null;
|
|
2347
|
+
const name = sourceLine ? readString(sourceLine, "name") : null;
|
|
2348
|
+
const kind = sourceLine ? readSalesOrderLineKind(sourceLine.kind) : null;
|
|
2349
|
+
const currencyCode = sourceLine ? readString(sourceLine, "currency_code") : null;
|
|
2350
|
+
const unitPriceNet = sourceLine ? readString(sourceLine, "unit_price_net") : null;
|
|
2351
|
+
const unitPriceGross = sourceLine ? readString(sourceLine, "unit_price_gross") : null;
|
|
2352
|
+
const taxRate = sourceLine ? readString(sourceLine, "tax_rate") : null;
|
|
2353
|
+
const originalPricingIncomplete = input.pricing === "original" && (unitPriceNet === null || unitPriceGross === null || taxRate === null);
|
|
2354
|
+
if (!sourceLine || sourceLine.order_id !== claim.orderId || !productId && !productVariantId && !name || !kind || !currencyCode || originalPricingIncomplete) {
|
|
2355
|
+
skippedLineIds.push(line.id);
|
|
2356
|
+
continue;
|
|
2357
|
+
}
|
|
2358
|
+
const replacementLine = {
|
|
2359
|
+
kind,
|
|
2360
|
+
currencyCode,
|
|
2361
|
+
quantity: String(effectiveQty),
|
|
2362
|
+
unitPriceNet: input.pricing === "zero" ? "0" : unitPriceNet,
|
|
2363
|
+
unitPriceGross: input.pricing === "zero" ? "0" : unitPriceGross
|
|
2364
|
+
};
|
|
2365
|
+
if (productId) replacementLine.productId = productId;
|
|
2366
|
+
if (productVariantId) replacementLine.productVariantId = productVariantId;
|
|
2367
|
+
if (name) replacementLine.name = name;
|
|
2368
|
+
if (input.pricing === "original") replacementLine.taxRate = taxRate;
|
|
2369
|
+
replacementLines.push(replacementLine);
|
|
2370
|
+
}
|
|
2371
|
+
if (replacementLines.length === 0) {
|
|
2372
|
+
throw new CrudHttpError(400, { error: "warranty_claims.errors.replacementNoEligibleLines" });
|
|
2373
|
+
}
|
|
2374
|
+
const orderInput = {
|
|
2375
|
+
organizationId: scope.organizationId,
|
|
2376
|
+
tenantId: scope.tenantId,
|
|
2377
|
+
currencyCode: orderCurrencyCode,
|
|
2378
|
+
metadata: { warrantyClaimId: claim.id, warrantyClaimNumber: claim.claimNumber },
|
|
2379
|
+
lines: replacementLines
|
|
2380
|
+
};
|
|
2381
|
+
const customerEntityId = readString(sourceOrder, "customer_entity_id");
|
|
2382
|
+
const customerContactId = readString(sourceOrder, "customer_contact_id");
|
|
2383
|
+
const billingAddressId = readString(sourceOrder, "billing_address_id");
|
|
2384
|
+
const shippingAddressId = readString(sourceOrder, "shipping_address_id");
|
|
2385
|
+
const channelId = readString(sourceOrder, "channel_id");
|
|
2386
|
+
if (customerEntityId) orderInput.customerEntityId = customerEntityId;
|
|
2387
|
+
if (customerContactId) orderInput.customerContactId = customerContactId;
|
|
2388
|
+
if (billingAddressId) orderInput.billingAddressId = billingAddressId;
|
|
2389
|
+
if (shippingAddressId) orderInput.shippingAddressId = shippingAddressId;
|
|
2390
|
+
if (channelId) orderInput.channelId = channelId;
|
|
2391
|
+
const commandBus = ctx.container.resolve("commandBus");
|
|
2392
|
+
const { result: dispatchedResult } = await commandBus.execute(
|
|
2393
|
+
"sales.orders.create",
|
|
2394
|
+
{ input: orderInput, ctx: scrubbedDispatchContext(ctx) }
|
|
2395
|
+
);
|
|
2396
|
+
if (!dispatchedResult?.orderId) {
|
|
2397
|
+
throw new CrudHttpError(400, { error: "warranty_claims.errors.save_failed" });
|
|
2398
|
+
}
|
|
2399
|
+
const replacementOrderId = dispatchedResult.orderId;
|
|
2400
|
+
let replacementOrderUpdatedAt = null;
|
|
2401
|
+
try {
|
|
2402
|
+
const createdOrderRow = await querySalesReferenceRow(ctx, "sales_orders", scope, replacementOrderId, ["id", "updated_at"]);
|
|
2403
|
+
replacementOrderUpdatedAt = createdOrderRow?.updated_at ? new Date(createdOrderRow.updated_at).toISOString() : null;
|
|
2404
|
+
await em.transactional(async (tx) => {
|
|
2405
|
+
const lockedClaim = await requireScopedClaim(tx, claim.id, scope, { lockMode: LockMode.PESSIMISTIC_WRITE });
|
|
2406
|
+
if (lockedClaim.replacementOrderId) {
|
|
2407
|
+
throw new CrudHttpError(400, { error: "warranty_claims.errors.replacementAlreadyLinked" });
|
|
2408
|
+
}
|
|
2409
|
+
lockedClaim.replacementOrderId = replacementOrderId;
|
|
2410
|
+
if (ADVANCE_REPLACEMENT_CLAIM_STATUSES.has(lockedClaim.status)) {
|
|
2411
|
+
lockedClaim.advanceReplacement = true;
|
|
2412
|
+
}
|
|
2413
|
+
lockedClaim.updatedAt = /* @__PURE__ */ new Date();
|
|
2414
|
+
appendClaimEvent(tx, lockedClaim, "system", {
|
|
2415
|
+
visibility: "internal",
|
|
2416
|
+
payload: { action: "replacement_order_created", replacementOrderId, pricing: input.pricing },
|
|
2417
|
+
actorUserId: ctx.auth?.sub ?? null
|
|
2418
|
+
});
|
|
2419
|
+
claim.replacementOrderId = replacementOrderId;
|
|
2420
|
+
claim.advanceReplacement = lockedClaim.advanceReplacement;
|
|
2421
|
+
claim.updatedAt = lockedClaim.updatedAt;
|
|
2422
|
+
await tx.flush();
|
|
2423
|
+
});
|
|
2424
|
+
} catch (err) {
|
|
2425
|
+
try {
|
|
2426
|
+
await dispatchSalesOrderDelete(ctx, scope, replacementOrderId);
|
|
2427
|
+
} catch (compensationErr) {
|
|
2428
|
+
replacementOrderBridgeLogger.error("warranty_claims.claim.create_replacement_order compensation failed \u2014 orphaned sales order", {
|
|
2429
|
+
err: compensationErr,
|
|
2430
|
+
claimId: claim.id,
|
|
2431
|
+
replacementOrderId
|
|
2432
|
+
});
|
|
2433
|
+
try {
|
|
2434
|
+
const orphanEm = ctx.container.resolve("em").fork();
|
|
2435
|
+
const orphanClaim = await findOneWithDecryption(
|
|
2436
|
+
orphanEm,
|
|
2437
|
+
WarrantyClaim,
|
|
2438
|
+
{ id: claim.id, tenantId: scope.tenantId, organizationId: scope.organizationId, deletedAt: null },
|
|
2439
|
+
{},
|
|
2440
|
+
scope
|
|
2441
|
+
);
|
|
2442
|
+
if (orphanClaim) {
|
|
2443
|
+
appendClaimEvent(orphanEm, orphanClaim, "system", {
|
|
2444
|
+
visibility: "internal",
|
|
2445
|
+
payload: { action: "replacement_order_orphaned", replacementOrderId },
|
|
2446
|
+
actorUserId: ctx.auth?.sub ?? null
|
|
2447
|
+
});
|
|
2448
|
+
await orphanEm.flush();
|
|
2449
|
+
}
|
|
2450
|
+
} catch (orphanEventErr) {
|
|
2451
|
+
replacementOrderBridgeLogger.error("warranty_claims.claim.create_replacement_order orphan timeline write failed", {
|
|
2452
|
+
err: orphanEventErr,
|
|
2453
|
+
claimId: claim.id,
|
|
2454
|
+
replacementOrderId
|
|
2455
|
+
});
|
|
2456
|
+
}
|
|
2457
|
+
throw new CrudHttpError(500, { error: "warranty_claims.errors.save_failed" });
|
|
2458
|
+
}
|
|
2459
|
+
throw err;
|
|
2460
|
+
}
|
|
2461
|
+
await emitClaimCrud(ctx, "updated", claim);
|
|
2462
|
+
return {
|
|
2463
|
+
claimId: claim.id,
|
|
2464
|
+
replacementOrderId,
|
|
2465
|
+
replacementOrderUpdatedAt,
|
|
2466
|
+
skippedLineIds,
|
|
2467
|
+
pricing: input.pricing
|
|
2468
|
+
};
|
|
2469
|
+
},
|
|
2470
|
+
captureAfter: async (rawInput, result, ctx) => {
|
|
2471
|
+
const input = parseCommandInput(claimCreateReplacementOrderSchema, rawInput);
|
|
2472
|
+
const scope = resolveScope(ctx, input);
|
|
2473
|
+
const em = ctx.container.resolve("em").fork();
|
|
2474
|
+
return loadClaimSnapshot(em, result.claimId, scope);
|
|
2475
|
+
},
|
|
2476
|
+
buildLog: async ({ result, snapshots }) => {
|
|
2477
|
+
const base = buildClaimLog("warranty_claims.audit.claim.create_replacement_order", result.claimId, snapshots);
|
|
2478
|
+
const undoPayload = {
|
|
2479
|
+
...base.payload.undo,
|
|
2480
|
+
replacementOrder: {
|
|
2481
|
+
id: result.replacementOrderId,
|
|
2482
|
+
updatedAt: result.replacementOrderUpdatedAt
|
|
2483
|
+
}
|
|
2484
|
+
};
|
|
2485
|
+
return { ...base, payload: { undo: undoPayload } };
|
|
2486
|
+
},
|
|
2487
|
+
undo: async ({ logEntry, ctx }) => {
|
|
2488
|
+
const payload = extractUndoPayload(logEntry);
|
|
2489
|
+
const before = payload?.before;
|
|
2490
|
+
if (!before) return;
|
|
2491
|
+
const after = payload?.after;
|
|
2492
|
+
if (!after) throwClaimUndoConflict();
|
|
2493
|
+
await assertUndoSnapshotCurrent(ctx, after);
|
|
2494
|
+
const scope = { tenantId: before.tenantId, organizationId: before.organizationId };
|
|
2495
|
+
const replacementOrder = payload?.replacementOrder ?? null;
|
|
2496
|
+
if (replacementOrder?.id) {
|
|
2497
|
+
const currentRow = await querySalesReferenceRow(ctx, "sales_orders", scope, replacementOrder.id, ["id", "updated_at"]);
|
|
2498
|
+
if (currentRow === void 0) {
|
|
2499
|
+
throw new CrudHttpError(409, { error: "warranty_claims.errors.replacementOrderChangedUndoAborted" });
|
|
2500
|
+
}
|
|
2501
|
+
if (currentRow) {
|
|
2502
|
+
const currentUpdatedAt = currentRow.updated_at ? new Date(currentRow.updated_at).toISOString() : null;
|
|
2503
|
+
if (!replacementOrder.updatedAt || !currentUpdatedAt || currentUpdatedAt !== replacementOrder.updatedAt) {
|
|
2504
|
+
throw new CrudHttpError(409, { error: "warranty_claims.errors.replacementOrderChangedUndoAborted" });
|
|
2505
|
+
}
|
|
2506
|
+
await dispatchSalesOrderDelete(ctx, scope, replacementOrder.id);
|
|
2507
|
+
} else {
|
|
2508
|
+
replacementOrderBridgeLogger.info("warranty_claims.claim.create_replacement_order undo found replacement order already absent", {
|
|
2509
|
+
claimId: before.id,
|
|
2510
|
+
replacementOrderId: replacementOrder.id
|
|
2511
|
+
});
|
|
2512
|
+
}
|
|
2513
|
+
}
|
|
2514
|
+
const em = ctx.container.resolve("em").fork();
|
|
2515
|
+
let claim;
|
|
2516
|
+
await withAtomicFlush(em, [
|
|
2517
|
+
async () => {
|
|
2518
|
+
claim = await restoreSnapshot(em, before, after);
|
|
2519
|
+
appendClaimEvent(em, claim, "system", {
|
|
2520
|
+
visibility: "internal",
|
|
2521
|
+
payload: { action: "undo_replacement_order_created" },
|
|
2522
|
+
actorUserId: ctx.auth?.sub ?? null
|
|
2523
|
+
});
|
|
2524
|
+
}
|
|
2525
|
+
], { transaction: true, label: "warranty_claims.claim.create_replacement_order.undo" });
|
|
2526
|
+
await emitClaimUndoCrud(ctx, "updated", claim);
|
|
2527
|
+
}
|
|
2528
|
+
};
|
|
2529
|
+
registerCommand(createClaimCommand);
|
|
2530
|
+
registerCommand(updateClaimCommand);
|
|
2531
|
+
registerCommand(deleteClaimCommand);
|
|
2532
|
+
registerCommand(submitClaimCommand);
|
|
2533
|
+
registerCommand(transitionClaimCommand);
|
|
2534
|
+
registerCommand(assignClaimCommand);
|
|
2535
|
+
registerCommand(setReturnLabelCommand);
|
|
2536
|
+
registerCommand(escalateClaimCommand);
|
|
2537
|
+
registerCommand(commentClaimCommand);
|
|
2538
|
+
registerCommand(createVendorRecoveryCommand);
|
|
2539
|
+
registerCommand(createSalesReturnCommand);
|
|
2540
|
+
registerCommand(createCreditMemoCommand);
|
|
2541
|
+
registerCommand(createReplacementOrderCommand);
|
|
2542
|
+
const claimCommands = [
|
|
2543
|
+
createClaimCommand,
|
|
2544
|
+
updateClaimCommand,
|
|
2545
|
+
deleteClaimCommand,
|
|
2546
|
+
submitClaimCommand,
|
|
2547
|
+
transitionClaimCommand,
|
|
2548
|
+
assignClaimCommand,
|
|
2549
|
+
setReturnLabelCommand,
|
|
2550
|
+
escalateClaimCommand,
|
|
2551
|
+
commentClaimCommand,
|
|
2552
|
+
createVendorRecoveryCommand,
|
|
2553
|
+
createSalesReturnCommand,
|
|
2554
|
+
createCreditMemoCommand,
|
|
2555
|
+
createReplacementOrderCommand
|
|
2556
|
+
];
|
|
2557
|
+
export {
|
|
2558
|
+
assertClaimedQtyWithinSold,
|
|
2559
|
+
assertPendingClaimQuantitiesWithinSold,
|
|
2560
|
+
assignClaimCommand,
|
|
2561
|
+
claimCommands,
|
|
2562
|
+
claimCrudEvents,
|
|
2563
|
+
commentClaimCommand,
|
|
2564
|
+
createClaimCommand,
|
|
2565
|
+
createCreditMemoCommand,
|
|
2566
|
+
createReplacementOrderCommand,
|
|
2567
|
+
createSalesReturnCommand,
|
|
2568
|
+
createVendorRecoveryCommand,
|
|
2569
|
+
deleteClaimCommand,
|
|
2570
|
+
escalateClaimCommand,
|
|
2571
|
+
setReturnLabelCommand,
|
|
2572
|
+
submitClaimCommand,
|
|
2573
|
+
transitionClaimCommand,
|
|
2574
|
+
updateClaimCommand,
|
|
2575
|
+
validateClaimReferences
|
|
2576
|
+
};
|
|
2577
|
+
//# sourceMappingURL=claims.js.map
|