@openlup/core 0.0.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (192) hide show
  1. package/CHANGELOG.md +184 -0
  2. package/CODE_OF_CONDUCT.md +39 -0
  3. package/CONTRIBUTING.md +34 -0
  4. package/LICENSE +204 -0
  5. package/MAINTAINERS.md +33 -0
  6. package/README.md +64 -3
  7. package/SECURITY.md +29 -0
  8. package/dist/bundle/contracts.d.ts +9 -0
  9. package/dist/bundle/contracts.js +1 -0
  10. package/dist/bundle/ports.d.ts +41 -0
  11. package/dist/bundle/ports.js +1 -0
  12. package/dist/catalog/identifiers.d.ts +31 -0
  13. package/dist/catalog/identifiers.js +46 -0
  14. package/dist/catalog/index.d.ts +2 -0
  15. package/dist/catalog/index.js +2 -0
  16. package/dist/checkout/idempotency.d.ts +29 -0
  17. package/dist/checkout/idempotency.js +37 -0
  18. package/dist/checkout/index.d.ts +2 -0
  19. package/dist/checkout/index.js +2 -0
  20. package/dist/company-identity/contracts.d.ts +220 -0
  21. package/dist/company-identity/contracts.js +168 -0
  22. package/dist/company-identity/index.d.ts +6 -0
  23. package/dist/company-identity/index.js +2 -0
  24. package/dist/company-identity/ports.d.ts +13 -0
  25. package/dist/company-identity/ports.js +1 -0
  26. package/dist/fulfillment/facts.d.ts +103 -0
  27. package/dist/fulfillment/facts.js +195 -0
  28. package/dist/fulfillment/index.d.ts +4 -0
  29. package/dist/fulfillment/index.js +2 -0
  30. package/dist/inventory/atp.d.ts +172 -0
  31. package/dist/inventory/atp.js +211 -0
  32. package/dist/inventory/index.d.ts +2 -0
  33. package/dist/inventory/index.js +2 -0
  34. package/dist/marketing/research/contracts.d.ts +75 -0
  35. package/dist/marketing/research/contracts.js +60 -0
  36. package/dist/marketing/research/index.d.ts +4 -0
  37. package/dist/marketing/research/index.js +4 -0
  38. package/dist/marketing/research/ports.d.ts +15 -0
  39. package/dist/marketing/research/ports.js +1 -0
  40. package/dist/partners/index.d.ts +4 -0
  41. package/dist/partners/index.js +4 -0
  42. package/dist/partners/ports.d.ts +12 -0
  43. package/dist/partners/ports.js +1 -0
  44. package/dist/partners/status.d.ts +48 -0
  45. package/dist/partners/status.js +35 -0
  46. package/dist/payment/executionBaseContracts.d.ts +81 -0
  47. package/dist/payment/executionBaseContracts.js +8 -0
  48. package/dist/payment/index.d.ts +36 -0
  49. package/dist/payment/index.js +18 -0
  50. package/dist/payment/methodLifecycle.d.ts +91 -0
  51. package/dist/payment/methodLifecycle.js +76 -0
  52. package/dist/payment/paymentControlTypes.d.ts +135 -0
  53. package/dist/payment/paymentControlTypes.js +110 -0
  54. package/dist/payment/paymentFailureTaxonomy.d.ts +80 -0
  55. package/dist/payment/paymentFailureTaxonomy.js +155 -0
  56. package/dist/payment/paymentFailureTaxonomyContracts.d.ts +106 -0
  57. package/dist/payment/paymentFailureTaxonomyContracts.js +177 -0
  58. package/dist/payment/paymentRecovery.d.ts +26 -0
  59. package/dist/payment/paymentRecovery.js +73 -0
  60. package/dist/payment/paymentRecoveryContracts.d.ts +53 -0
  61. package/dist/payment/paymentRecoveryContracts.js +10 -0
  62. package/dist/payment/paymentStateMachine.d.ts +28 -0
  63. package/dist/payment/paymentStateMachine.js +184 -0
  64. package/dist/payment/providerAttemptIdempotency.d.ts +23 -0
  65. package/dist/payment/providerAttemptIdempotency.js +31 -0
  66. package/dist/payment/providerCapability.d.ts +148 -0
  67. package/dist/payment/providerCapability.js +13 -0
  68. package/dist/platform-runtime/contracts.d.ts +9 -0
  69. package/dist/platform-runtime/contracts.js +13 -0
  70. package/dist/platform-runtime/index.d.ts +10 -0
  71. package/dist/platform-runtime/index.js +4 -0
  72. package/dist/platform-runtime/platformKernel.d.ts +44 -0
  73. package/dist/platform-runtime/platformKernel.js +39 -0
  74. package/dist/platform-runtime/ports.d.ts +100 -0
  75. package/dist/platform-runtime/ports.js +1 -0
  76. package/dist/pricing/index.d.ts +8 -0
  77. package/dist/pricing/index.js +8 -0
  78. package/dist/pricing/ports.d.ts +22 -0
  79. package/dist/pricing/ports.js +7 -0
  80. package/dist/pricing/pricingBreakdown.d.ts +86 -0
  81. package/dist/pricing/pricingBreakdown.js +155 -0
  82. package/dist/pricing/targetPriceAllocation.d.ts +97 -0
  83. package/dist/pricing/targetPriceAllocation.js +202 -0
  84. package/dist/pricing/types.d.ts +32 -0
  85. package/dist/pricing/types.js +4 -0
  86. package/dist/promo/adjustmentEngine.d.ts +4 -0
  87. package/dist/promo/adjustmentEngine.js +194 -0
  88. package/dist/promo/adjustmentTypes.d.ts +103 -0
  89. package/dist/promo/adjustmentTypes.js +13 -0
  90. package/dist/promo/index.d.ts +8 -0
  91. package/dist/promo/index.js +8 -0
  92. package/dist/promo/promoEvaluator.d.ts +25 -0
  93. package/dist/promo/promoEvaluator.js +135 -0
  94. package/dist/promo/types.d.ts +86 -0
  95. package/dist/promo/types.js +12 -0
  96. package/dist/risk/core/evaluator.d.ts +3 -0
  97. package/dist/risk/core/evaluator.js +32 -0
  98. package/dist/risk/core/rules.d.ts +3 -0
  99. package/dist/risk/core/rules.js +118 -0
  100. package/dist/risk/core/types.d.ts +65 -0
  101. package/dist/risk/core/types.js +1 -0
  102. package/dist/risk/index.d.ts +8 -0
  103. package/dist/risk/index.js +8 -0
  104. package/dist/risk/types.d.ts +32 -0
  105. package/dist/risk/types.js +43 -0
  106. package/dist/shipping/index.d.ts +4 -0
  107. package/dist/shipping/index.js +4 -0
  108. package/dist/shipping/shippingResolver.d.ts +26 -0
  109. package/dist/shipping/shippingResolver.js +88 -0
  110. package/dist/shipping/types.d.ts +45 -0
  111. package/dist/shipping/types.js +4 -0
  112. package/dist/subscription/cycleHardening.d.ts +72 -0
  113. package/dist/subscription/cycleHardening.js +95 -0
  114. package/dist/subscription/index.d.ts +14 -0
  115. package/dist/subscription/index.js +14 -0
  116. package/dist/subscription/subscriptionEngineCore.d.ts +13 -0
  117. package/dist/subscription/subscriptionEngineCore.js +145 -0
  118. package/dist/subscription/subscriptionEngineLifecycle.d.ts +52 -0
  119. package/dist/subscription/subscriptionEngineLifecycle.js +196 -0
  120. package/dist/subscription/subscriptionEnginePayment.d.ts +19 -0
  121. package/dist/subscription/subscriptionEnginePayment.js +127 -0
  122. package/dist/subscription/subscriptionEngineTypes.d.ts +94 -0
  123. package/dist/subscription/subscriptionEngineTypes.js +4 -0
  124. package/dist/subscription/subscriptionEngineUtils.d.ts +32 -0
  125. package/dist/subscription/subscriptionEngineUtils.js +74 -0
  126. package/dist/subscription/types.d.ts +58 -0
  127. package/dist/subscription/types.js +47 -0
  128. package/dist/testing.d.ts +48 -0
  129. package/dist/testing.js +105 -0
  130. package/package.json +215 -4
  131. package/release-gates.json +232 -0
  132. package/src/bundle/contracts.ts +15 -0
  133. package/src/bundle/ports.ts +32 -0
  134. package/src/catalog/identifiers.ts +50 -0
  135. package/src/catalog/index.ts +2 -0
  136. package/src/checkout/idempotency.ts +49 -0
  137. package/src/checkout/index.ts +2 -0
  138. package/src/company-identity/contracts.ts +209 -0
  139. package/src/company-identity/index.ts +27 -0
  140. package/src/company-identity/ports.ts +19 -0
  141. package/src/fulfillment/facts.ts +300 -0
  142. package/src/fulfillment/index.ts +34 -0
  143. package/src/inventory/atp.ts +249 -0
  144. package/src/inventory/index.ts +2 -0
  145. package/src/marketing/research/contracts.ts +91 -0
  146. package/src/marketing/research/index.ts +4 -0
  147. package/src/marketing/research/ports.ts +26 -0
  148. package/src/partners/index.ts +4 -0
  149. package/src/partners/ports.ts +18 -0
  150. package/src/partners/status.ts +49 -0
  151. package/src/payment/executionBaseContracts.ts +93 -0
  152. package/src/payment/index.ts +128 -0
  153. package/src/payment/methodLifecycle.ts +130 -0
  154. package/src/payment/paymentControlTypes.ts +197 -0
  155. package/src/payment/paymentFailureTaxonomy.ts +201 -0
  156. package/src/payment/paymentFailureTaxonomyContracts.ts +228 -0
  157. package/src/payment/paymentRecovery.ts +95 -0
  158. package/src/payment/paymentRecoveryContracts.ts +61 -0
  159. package/src/payment/paymentStateMachine.ts +263 -0
  160. package/src/payment/providerAttemptIdempotency.ts +57 -0
  161. package/src/payment/providerCapability.ts +172 -0
  162. package/src/platform-runtime/contracts.ts +33 -0
  163. package/src/platform-runtime/index.ts +36 -0
  164. package/src/platform-runtime/platformKernel.ts +86 -0
  165. package/src/platform-runtime/ports.ts +88 -0
  166. package/src/pricing/index.ts +8 -0
  167. package/src/pricing/ports.ts +27 -0
  168. package/src/pricing/pricingBreakdown.ts +243 -0
  169. package/src/pricing/targetPriceAllocation.ts +293 -0
  170. package/src/pricing/types.ts +36 -0
  171. package/src/promo/adjustmentEngine.ts +253 -0
  172. package/src/promo/adjustmentTypes.ts +94 -0
  173. package/src/promo/index.ts +8 -0
  174. package/src/promo/promoEvaluator.ts +137 -0
  175. package/src/promo/types.ts +97 -0
  176. package/src/risk/core/evaluator.ts +41 -0
  177. package/src/risk/core/rules.ts +128 -0
  178. package/src/risk/core/types.ts +73 -0
  179. package/src/risk/index.ts +8 -0
  180. package/src/risk/types.ts +60 -0
  181. package/src/shipping/index.ts +4 -0
  182. package/src/shipping/shippingResolver.ts +101 -0
  183. package/src/shipping/types.ts +49 -0
  184. package/src/subscription/cycleHardening.ts +141 -0
  185. package/src/subscription/index.ts +14 -0
  186. package/src/subscription/subscriptionEngineCore.ts +181 -0
  187. package/src/subscription/subscriptionEngineLifecycle.ts +282 -0
  188. package/src/subscription/subscriptionEnginePayment.ts +174 -0
  189. package/src/subscription/subscriptionEngineTypes.ts +112 -0
  190. package/src/subscription/subscriptionEngineUtils.ts +110 -0
  191. package/src/subscription/types.ts +104 -0
  192. package/src/testing.ts +169 -0
@@ -0,0 +1,300 @@
1
+ import { z } from "zod";
2
+
3
+ /** @beta */ export const NORMALIZED_FULFILLMENT_FACT_SCHEMA_VERSION = 1 as const;
4
+ /** @beta */ export const NORMALIZED_FULFILLMENT_FACT_PREVIOUS_SCHEMA_VERSION = 0 as const;
5
+ /** @beta */ export const PROVIDER_FULFILLMENT_FACT_SOURCES = [
6
+ "provider_webhook",
7
+ "provider_reconciliation",
8
+ "provider_response",
9
+ ] as const;
10
+ /** @beta */ export const FULFILLMENT_FACT_SOURCES = [
11
+ ...PROVIDER_FULFILLMENT_FACT_SOURCES,
12
+ "operator_repair",
13
+ ] as const;
14
+ /** @beta */ export const FULFILLMENT_FACT_SOURCE_AUTHORITIES = [
15
+ "provider_asserted",
16
+ "operator_asserted",
17
+ ] as const;
18
+ /** @beta */ export type ProviderFulfillmentFactSource =
19
+ (typeof PROVIDER_FULFILLMENT_FACT_SOURCES)[number];
20
+ /** @beta */ export type FulfillmentFactSource = (typeof FULFILLMENT_FACT_SOURCES)[number];
21
+ /** @beta */ export type FulfillmentFactSourceAuthority =
22
+ (typeof FULFILLMENT_FACT_SOURCE_AUTHORITIES)[number];
23
+ /** @beta */ export type FulfillmentFactProvenance =
24
+ | { readonly source: ProviderFulfillmentFactSource; readonly authority: "provider_asserted" }
25
+ | { readonly source: "operator_repair"; readonly authority: "operator_asserted" };
26
+
27
+ /**
28
+ * Pointer to separately governed raw evidence. The reference is minted by the
29
+ * adopting application (`evidence:<uuid>`), never copied from provider,
30
+ * tracking, customer, or order data. PII safety still belongs to that minting
31
+ * boundary and the referenced evidence store; this contract only enforces the
32
+ * namespace and identifier shape.
33
+ */
34
+ /** @beta */ export interface FulfillmentRawEventReference {
35
+ readonly reference: `evidence:${string}`;
36
+ readonly digestAlgorithm: "sha256";
37
+ readonly digest: string;
38
+ }
39
+
40
+ /** Comparable only within one provider + fulfillment reference sequence. */
41
+ /** @beta */ export interface FulfillmentProviderSequence {
42
+ readonly kind: "monotonic_ordinal";
43
+ readonly ordinal: number;
44
+ }
45
+
46
+ interface NormalizedFulfillmentFactFields<TCanonicalStatus extends string> {
47
+ readonly mapperVersion: string;
48
+ readonly factId: string;
49
+ readonly providerKind: string;
50
+ readonly fulfillmentReference: string;
51
+ readonly canonicalStatus: TCanonicalStatus;
52
+ readonly provenance: FulfillmentFactProvenance;
53
+ readonly occurredAt: string | null;
54
+ readonly receivedAt: string;
55
+ readonly providerSequence?: FulfillmentProviderSequence;
56
+ }
57
+
58
+ /** Provider-neutral evidence; it authorizes no transition or external effect. */
59
+ /** @beta */ export interface NormalizedFulfillmentFactV1<TCanonicalStatus extends string = string>
60
+ extends NormalizedFulfillmentFactFields<TCanonicalStatus> {
61
+ readonly schemaVersion: typeof NORMALIZED_FULFILLMENT_FACT_SCHEMA_VERSION;
62
+ readonly rawEvent: FulfillmentRawEventReference;
63
+ }
64
+
65
+ /** @beta */ export type NormalizedFulfillmentFact<TCanonicalStatus extends string = string> =
66
+ NormalizedFulfillmentFactV1<TCanonicalStatus>;
67
+
68
+ /** Narrow legacy wire shape supported only for deterministic N-1 ingestion. */
69
+ /** @beta */ export interface NormalizedFulfillmentFactV0<TCanonicalStatus extends string = string>
70
+ extends NormalizedFulfillmentFactFields<TCanonicalStatus> {
71
+ readonly schemaVersion: typeof NORMALIZED_FULFILLMENT_FACT_PREVIOUS_SCHEMA_VERSION;
72
+ readonly rawEventReference: `evidence:${string}`;
73
+ readonly rawEventSha256: string;
74
+ }
75
+
76
+ /** @beta */ export type CompatibleNormalizedFulfillmentFact<TCanonicalStatus extends string = string> =
77
+ | NormalizedFulfillmentFactV0<TCanonicalStatus>
78
+ | NormalizedFulfillmentFactV1<TCanonicalStatus>;
79
+ /** @beta */ export interface FulfillmentFactParserOptions<TCanonicalStatus extends string> {
80
+ isCanonicalStatus(value: string): value is TCanonicalStatus;
81
+ }
82
+
83
+ const nonEmpty = z.string().trim().min(1);
84
+ const opaqueId = z.string().regex(/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/u);
85
+ const evidenceReference = z.string().regex(
86
+ /^evidence:[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/u,
87
+ "must be an application-minted evidence:<uuid> reference",
88
+ );
89
+ const sha256 = z.string().regex(/^[a-f0-9]{64}$/u);
90
+ const rfc3339 = z.iso.datetime({ offset: true });
91
+ const providerProvenance = z.object({
92
+ source: z.enum(PROVIDER_FULFILLMENT_FACT_SOURCES),
93
+ authority: z.literal("provider_asserted"),
94
+ }).strict();
95
+ const operatorProvenance = z.object({
96
+ source: z.literal("operator_repair"),
97
+ authority: z.literal("operator_asserted"),
98
+ }).strict();
99
+ const provenance = z.union([providerProvenance, operatorProvenance]);
100
+ const providerSequence = z.object({
101
+ kind: z.literal("monotonic_ordinal"),
102
+ ordinal: z.number().int().nonnegative().safe(),
103
+ }).strict();
104
+ const commonFields = {
105
+ mapperVersion: nonEmpty,
106
+ factId: opaqueId,
107
+ providerKind: opaqueId,
108
+ fulfillmentReference: opaqueId,
109
+ canonicalStatus: nonEmpty,
110
+ provenance,
111
+ occurredAt: rfc3339.nullable(),
112
+ receivedAt: rfc3339,
113
+ providerSequence: providerSequence.optional(),
114
+ } as const;
115
+ const v1Schema = z.object({
116
+ schemaVersion: z.literal(NORMALIZED_FULFILLMENT_FACT_SCHEMA_VERSION),
117
+ ...commonFields,
118
+ rawEvent: z.object({
119
+ reference: evidenceReference,
120
+ digestAlgorithm: z.literal("sha256"),
121
+ digest: sha256,
122
+ }).strict(),
123
+ }).strict();
124
+ const v0Schema = z.object({
125
+ schemaVersion: z.literal(NORMALIZED_FULFILLMENT_FACT_PREVIOUS_SCHEMA_VERSION),
126
+ ...commonFields,
127
+ rawEventReference: evidenceReference,
128
+ rawEventSha256: sha256,
129
+ }).strict();
130
+
131
+ /** @beta */ export class InvalidFulfillmentFactError extends Error {
132
+ constructor(readonly reason: string) {
133
+ super(`invalid_fulfillment_fact:${reason}`);
134
+ this.name = "InvalidFulfillmentFactError";
135
+ }
136
+ }
137
+
138
+ /** @beta */ export class UnsupportedFulfillmentFactSchemaError extends Error {
139
+ constructor(readonly schemaVersion: unknown) {
140
+ super(`unsupported_fulfillment_fact_schema:${String(schemaVersion)}`);
141
+ this.name = "UnsupportedFulfillmentFactSchemaError";
142
+ }
143
+ }
144
+
145
+ /** Strict public unknown-input boundary for current N and supported N-1. */
146
+ /** @beta */ export function parseNormalizedFulfillmentFact<TCanonicalStatus extends string>(
147
+ value: unknown,
148
+ options: FulfillmentFactParserOptions<TCanonicalStatus>,
149
+ ): NormalizedFulfillmentFact<TCanonicalStatus> {
150
+ const version = value && typeof value === "object"
151
+ ? (value as { schemaVersion?: unknown }).schemaVersion
152
+ : undefined;
153
+ const schema = version === NORMALIZED_FULFILLMENT_FACT_SCHEMA_VERSION
154
+ ? v1Schema
155
+ : version === NORMALIZED_FULFILLMENT_FACT_PREVIOUS_SCHEMA_VERSION
156
+ ? v0Schema
157
+ : null;
158
+ if (!schema) throw new UnsupportedFulfillmentFactSchemaError(version);
159
+ const parsed = schema.safeParse(value);
160
+ if (!parsed.success) throw new InvalidFulfillmentFactError(parsed.error.issues[0]?.message ?? "schema");
161
+ if (!options.isCanonicalStatus(parsed.data.canonicalStatus)) {
162
+ throw new InvalidFulfillmentFactError("canonical_status_unknown");
163
+ }
164
+ if (parsed.data.schemaVersion === NORMALIZED_FULFILLMENT_FACT_SCHEMA_VERSION) {
165
+ return parsed.data as NormalizedFulfillmentFact<TCanonicalStatus>;
166
+ }
167
+ const { rawEventReference, rawEventSha256, ...shared } = parsed.data;
168
+ return {
169
+ ...shared,
170
+ schemaVersion: NORMALIZED_FULFILLMENT_FACT_SCHEMA_VERSION,
171
+ canonicalStatus: parsed.data.canonicalStatus,
172
+ rawEvent: {
173
+ reference: rawEventReference,
174
+ digestAlgorithm: "sha256",
175
+ digest: rawEventSha256,
176
+ },
177
+ } as NormalizedFulfillmentFact<TCanonicalStatus>;
178
+ }
179
+
180
+ /** Stable property-order-independent identity of one already parsed fact. */
181
+ /** @beta */ export function canonicalFulfillmentFactFingerprint(
182
+ fact: NormalizedFulfillmentFact,
183
+ ): string {
184
+ return JSON.stringify([
185
+ fact.schemaVersion,
186
+ fact.mapperVersion,
187
+ fact.factId,
188
+ fact.providerKind,
189
+ fact.fulfillmentReference,
190
+ fact.canonicalStatus,
191
+ fact.provenance.source,
192
+ fact.provenance.authority,
193
+ fact.occurredAt,
194
+ fact.receivedAt,
195
+ fact.providerSequence?.kind ?? null,
196
+ fact.providerSequence?.ordinal ?? null,
197
+ fact.rawEvent.reference,
198
+ fact.rawEvent.digestAlgorithm,
199
+ fact.rawEvent.digest,
200
+ ]);
201
+ }
202
+
203
+ /** @beta */ export class FulfillmentFactCollisionError extends Error {
204
+ constructor(readonly factId: string) {
205
+ super(`fulfillment_fact_id_collision:${factId}`);
206
+ this.name = "FulfillmentFactCollisionError";
207
+ }
208
+ }
209
+
210
+ /** Deterministic exact-duplicate collapse; conflicting reuse fails closed. */
211
+ /** @beta */ export function canonicalizeFulfillmentFacts<TCanonicalStatus extends string>(
212
+ facts: readonly NormalizedFulfillmentFact<TCanonicalStatus>[],
213
+ ): NormalizedFulfillmentFact<TCanonicalStatus>[] {
214
+ const byId = new Map<string, { fact: NormalizedFulfillmentFact<TCanonicalStatus>; fingerprint: string }>();
215
+ for (const fact of facts) {
216
+ const fingerprint = canonicalFulfillmentFactFingerprint(fact);
217
+ const existing = byId.get(fact.factId);
218
+ if (existing && existing.fingerprint !== fingerprint) throw new FulfillmentFactCollisionError(fact.factId);
219
+ byId.set(fact.factId, { fact, fingerprint });
220
+ }
221
+ return [...byId.values()]
222
+ .sort((left, right) => left.fact.factId.localeCompare(right.fact.factId))
223
+ .map(({ fact }) => fact);
224
+ }
225
+
226
+ /** @beta */ export const TRANSITION_CONFLICT_REASONS = [
227
+ "status_regression",
228
+ "terminal_status_disagreement",
229
+ "provider_sequence_regression",
230
+ "source_authority_disagreement",
231
+ ] as const;
232
+ /** @beta */ export type TransitionConflictReason = (typeof TRANSITION_CONFLICT_REASONS)[number];
233
+
234
+ /** @beta */ export interface FulfillmentTransitionSnapshot<TCanonicalStatus extends string = string> {
235
+ readonly fact: NormalizedFulfillmentFact<TCanonicalStatus>;
236
+ }
237
+
238
+ /** @beta */ export type TransitionConflictDecision =
239
+ | { readonly kind: "selected"; readonly selected: "current" | "incoming"; readonly reasonCode: string }
240
+ | { readonly kind: "hold"; readonly reasonCode: string };
241
+
242
+ /** Self-contained immutable diagnostic emitted by a future consumer reducer. */
243
+ /** @beta */ export interface TransitionConflict<TCanonicalStatus extends string = string> {
244
+ readonly conflictId: `conflict:${string}`;
245
+ readonly policyVersion: string;
246
+ readonly reason: TransitionConflictReason;
247
+ readonly fulfillmentReference: string;
248
+ readonly current: FulfillmentTransitionSnapshot<TCanonicalStatus>;
249
+ readonly incoming: FulfillmentTransitionSnapshot<TCanonicalStatus>;
250
+ readonly decision: TransitionConflictDecision;
251
+ readonly detectedAt: string;
252
+ }
253
+
254
+ const conflictReference = z.string().regex(
255
+ /^conflict:[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/u,
256
+ "must be an application-minted conflict:<uuid> reference",
257
+ );
258
+ const transitionConflictEnvelope = z.object({
259
+ conflictId: conflictReference,
260
+ policyVersion: nonEmpty,
261
+ reason: z.enum(TRANSITION_CONFLICT_REASONS),
262
+ fulfillmentReference: opaqueId,
263
+ current: z.object({ fact: z.unknown() }).strict(),
264
+ incoming: z.object({ fact: z.unknown() }).strict(),
265
+ decision: z.discriminatedUnion("kind", [
266
+ z.object({
267
+ kind: z.literal("selected"),
268
+ selected: z.enum(["current", "incoming"]),
269
+ reasonCode: opaqueId,
270
+ }).strict(),
271
+ z.object({ kind: z.literal("hold"), reasonCode: opaqueId }).strict(),
272
+ ]),
273
+ detectedAt: rfc3339,
274
+ }).strict();
275
+
276
+ /** @beta */ export function parseTransitionConflict<TCanonicalStatus extends string>(
277
+ value: unknown,
278
+ options: FulfillmentFactParserOptions<TCanonicalStatus>,
279
+ ): TransitionConflict<TCanonicalStatus> {
280
+ const envelope = transitionConflictEnvelope.safeParse(value);
281
+ if (!envelope.success) {
282
+ throw new InvalidFulfillmentFactError(
283
+ `transition_conflict:${envelope.error.issues[0]?.message ?? "schema"}`,
284
+ );
285
+ }
286
+ const current = parseNormalizedFulfillmentFact(envelope.data.current.fact, options);
287
+ const incoming = parseNormalizedFulfillmentFact(envelope.data.incoming.fact, options);
288
+ if (current.providerKind !== incoming.providerKind
289
+ || current.fulfillmentReference !== incoming.fulfillmentReference
290
+ || envelope.data.fulfillmentReference !== current.fulfillmentReference) {
291
+ throw new InvalidFulfillmentFactError("transition_conflict_stream_mismatch");
292
+ }
293
+ return {
294
+ ...envelope.data,
295
+ conflictId: envelope.data.conflictId as `conflict:${string}`,
296
+ decision: envelope.data.decision!,
297
+ current: { fact: current },
298
+ incoming: { fact: incoming },
299
+ };
300
+ }
@@ -0,0 +1,34 @@
1
+ /** @beta */
2
+ export {
3
+ FULFILLMENT_FACT_SOURCE_AUTHORITIES,
4
+ FULFILLMENT_FACT_SOURCES,
5
+ PROVIDER_FULFILLMENT_FACT_SOURCES,
6
+ NORMALIZED_FULFILLMENT_FACT_PREVIOUS_SCHEMA_VERSION,
7
+ NORMALIZED_FULFILLMENT_FACT_SCHEMA_VERSION,
8
+ TRANSITION_CONFLICT_REASONS,
9
+ InvalidFulfillmentFactError,
10
+ FulfillmentFactCollisionError,
11
+ UnsupportedFulfillmentFactSchemaError,
12
+ canonicalFulfillmentFactFingerprint,
13
+ canonicalizeFulfillmentFacts,
14
+ parseNormalizedFulfillmentFact,
15
+ parseTransitionConflict,
16
+ } from "./facts.js";
17
+ /** @beta */
18
+ export type {
19
+ CompatibleNormalizedFulfillmentFact,
20
+ FulfillmentFactSourceAuthority,
21
+ FulfillmentFactSource,
22
+ FulfillmentFactProvenance,
23
+ FulfillmentRawEventReference,
24
+ FulfillmentProviderSequence,
25
+ FulfillmentFactParserOptions,
26
+ ProviderFulfillmentFactSource,
27
+ NormalizedFulfillmentFact,
28
+ NormalizedFulfillmentFactV0,
29
+ NormalizedFulfillmentFactV1,
30
+ TransitionConflict,
31
+ TransitionConflictDecision,
32
+ FulfillmentTransitionSnapshot,
33
+ TransitionConflictReason,
34
+ } from "./facts.js";
@@ -0,0 +1,249 @@
1
+ import { z } from "zod";
2
+
3
+ /** @beta */
4
+ export const INVENTORY_CONTRACT_VERSION = "inventory.v0";
5
+
6
+ const uuidSchema = z.guid();
7
+ const datetimeSchema = z.string().datetime({ offset: true });
8
+ const quantitySchema = z.number().int().nonnegative();
9
+ const positiveQuantitySchema = z.number().int().positive();
10
+
11
+ /** @beta */
12
+ export const inventoryLocationKindSchema = z.enum([
13
+ "internal_warehouse",
14
+ "third_party_logistics",
15
+ "supplier",
16
+ "quarantine",
17
+ "virtual",
18
+ ]);
19
+ /** @beta */
20
+ export const inventoryLocationStatusSchema = z.enum(["active", "inactive"]);
21
+ /** @beta */
22
+ export const inventoryLotStatusSchema = z.enum(["available", "quarantined", "expired", "recalled"]);
23
+ /** @beta */
24
+ export const inventoryAtpStatusSchema = z.enum(["fulfillable", "insufficient", "review_required", "unsupported"]);
25
+
26
+ /** @beta */
27
+ export const inventoryStockLineSchema = z.object({
28
+ skuId: uuidSchema,
29
+ sku: z.string().trim().min(1),
30
+ locationId: uuidSchema,
31
+ locationCode: z.string().trim().min(1),
32
+ locationKind: inventoryLocationKindSchema,
33
+ locationStatus: inventoryLocationStatusSchema,
34
+ fulfillable: z.boolean(),
35
+ lotId: uuidSchema.nullable(),
36
+ lotCode: z.string().trim().min(1).nullable(),
37
+ lotStatus: inventoryLotStatusSchema.nullable(),
38
+ expiresAt: datetimeSchema.nullable(),
39
+ onHand: quantitySchema,
40
+ reserved: quantitySchema,
41
+ unavailable: quantitySchema,
42
+ incoming: quantitySchema,
43
+ safetyStock: quantitySchema,
44
+ }).strict();
45
+
46
+ /** @beta */
47
+ export const inventoryAtpRequestLineSchema = z.object({
48
+ skuId: uuidSchema,
49
+ sku: z.string().trim().min(1),
50
+ quantity: positiveQuantitySchema,
51
+ }).strict();
52
+
53
+ /** @beta */
54
+ export const inventoryAtpRequestSchema = z.object({
55
+ contractVersion: z.literal(INVENTORY_CONTRACT_VERSION),
56
+ lines: z.array(inventoryAtpRequestLineSchema).min(1),
57
+ requestedAt: datetimeSchema,
58
+ orderMode: z.enum(["one_time", "subscription_cycle"]),
59
+ region: z.string().trim().min(2).max(32).optional(),
60
+ noSplitShipment: z.boolean().default(true),
61
+ minShelfLifeDays: z.number().int().min(0).max(730).default(0),
62
+ }).strict();
63
+
64
+ /** @beta */
65
+ export const inventoryAllocationSchema = z.object({
66
+ skuId: uuidSchema,
67
+ sku: z.string().trim().min(1),
68
+ locationId: uuidSchema,
69
+ locationCode: z.string().trim().min(1),
70
+ lotId: uuidSchema.nullable(),
71
+ lotCode: z.string().trim().min(1).nullable(),
72
+ quantity: positiveQuantitySchema,
73
+ expiresAt: datetimeSchema.nullable(),
74
+ }).strict();
75
+
76
+ /** @beta */
77
+ export const inventoryAtpLineResultSchema = z.object({
78
+ skuId: uuidSchema,
79
+ sku: z.string().trim().min(1),
80
+ requestedQuantity: positiveQuantitySchema,
81
+ availableQuantity: quantitySchema,
82
+ missingQuantity: quantitySchema,
83
+ status: inventoryAtpStatusSchema,
84
+ allocations: z.array(inventoryAllocationSchema),
85
+ }).strict();
86
+
87
+ /** @beta */
88
+ export const inventoryAtpResultSchema = z.object({
89
+ contractVersion: inventoryAtpRequestSchema.shape.contractVersion,
90
+ status: inventoryAtpStatusSchema,
91
+ reason: z.string().trim().min(1).nullable(),
92
+ locationId: uuidSchema.nullable(),
93
+ locationCode: z.string().trim().min(1).nullable(),
94
+ lines: z.array(inventoryAtpLineResultSchema),
95
+ }).strict();
96
+
97
+ /** @beta */
98
+ export type InventoryAtpRequest = z.input<typeof inventoryAtpRequestSchema>;
99
+ /** @beta */
100
+ export type InventoryAtpResult = z.infer<typeof inventoryAtpResultSchema>;
101
+ /** @beta */
102
+ export type InventoryStockLine = z.infer<typeof inventoryStockLineSchema>;
103
+
104
+ /** @beta */
105
+ export interface CalculateInventoryAtpInput {
106
+ request: InventoryAtpRequest;
107
+ stock: InventoryStockLine[];
108
+ }
109
+
110
+ /** @beta */
111
+ export function availableNow(line: Pick<InventoryStockLine, "onHand" | "reserved" | "unavailable" | "safetyStock">): number {
112
+ return Math.max(0, line.onHand - line.reserved - line.unavailable - line.safetyStock);
113
+ }
114
+
115
+ /** @beta */
116
+ export function calculateInventoryAtp(input: CalculateInventoryAtpInput): InventoryAtpResult {
117
+ if (input.request.noSplitShipment === false) {
118
+ return inventoryAtpResultSchema.parse({
119
+ contractVersion: INVENTORY_CONTRACT_VERSION,
120
+ status: "unsupported",
121
+ reason: "split_shipment_unsupported",
122
+ locationId: null,
123
+ locationCode: null,
124
+ lines: input.request.lines.map((line) => ({
125
+ skuId: line.skuId,
126
+ sku: line.sku,
127
+ requestedQuantity: line.quantity,
128
+ availableQuantity: 0,
129
+ missingQuantity: line.quantity,
130
+ status: "unsupported",
131
+ allocations: [],
132
+ })),
133
+ });
134
+ }
135
+
136
+ const usableStock = input.stock.filter((line) => isUsable(
137
+ line,
138
+ input.request.requestedAt,
139
+ input.request.minShelfLifeDays ?? 0,
140
+ ));
141
+ const locationIds = [...new Set(usableStock.map((line) => line.locationId))];
142
+ const preferredLocation = locationIds
143
+ .map((locationId) => allocationForLocation(input.request, usableStock, locationId))
144
+ .find((candidate) => candidate.status === "fulfillable");
145
+
146
+ if (preferredLocation) {
147
+ return inventoryAtpResultSchema.parse({
148
+ contractVersion: INVENTORY_CONTRACT_VERSION,
149
+ status: "fulfillable",
150
+ reason: null,
151
+ locationId: preferredLocation.locationId,
152
+ locationCode: preferredLocation.locationCode,
153
+ lines: preferredLocation.lines,
154
+ });
155
+ }
156
+
157
+ const lines = input.request.lines.map((requestLine) => {
158
+ const matching = usableStock.filter((line) => line.skuId === requestLine.skuId);
159
+ const availableQuantity = matching.reduce((sum, line) => sum + availableNow(line), 0);
160
+ return {
161
+ skuId: requestLine.skuId,
162
+ sku: requestLine.sku,
163
+ requestedQuantity: requestLine.quantity,
164
+ availableQuantity,
165
+ missingQuantity: Math.max(0, requestLine.quantity - availableQuantity),
166
+ status: availableQuantity >= requestLine.quantity ? "review_required" : "insufficient",
167
+ allocations: [],
168
+ };
169
+ });
170
+
171
+ const hasEnoughInAggregate = lines.every((line) => line.missingQuantity === 0);
172
+ return inventoryAtpResultSchema.parse({
173
+ contractVersion: INVENTORY_CONTRACT_VERSION,
174
+ status: hasEnoughInAggregate ? "review_required" : "insufficient",
175
+ reason: hasEnoughInAggregate
176
+ ? "stock_available_only_with_split_shipment"
177
+ : "insufficient_available_stock",
178
+ locationId: null,
179
+ locationCode: null,
180
+ lines,
181
+ });
182
+ }
183
+
184
+ function allocationForLocation(
185
+ request: InventoryAtpRequest,
186
+ stock: InventoryStockLine[],
187
+ locationId: string,
188
+ ) {
189
+ const locationStock = stock.filter((line) => line.locationId === locationId);
190
+ const locationCode = locationStock[0]?.locationCode ?? null;
191
+ const lines = request.lines.map((requestLine) => {
192
+ let remaining = requestLine.quantity;
193
+ const allocations = locationStock
194
+ .filter((line) => line.skuId === requestLine.skuId && availableNow(line) > 0)
195
+ .sort(fefoSort)
196
+ .flatMap((line) => {
197
+ if (remaining <= 0) return [];
198
+ const quantity = Math.min(remaining, availableNow(line));
199
+ remaining -= quantity;
200
+ return [{
201
+ skuId: requestLine.skuId,
202
+ sku: requestLine.sku,
203
+ locationId: line.locationId,
204
+ locationCode: line.locationCode,
205
+ lotId: line.lotId,
206
+ lotCode: line.lotCode,
207
+ quantity,
208
+ expiresAt: line.expiresAt,
209
+ }];
210
+ });
211
+
212
+ return {
213
+ skuId: requestLine.skuId,
214
+ sku: requestLine.sku,
215
+ requestedQuantity: requestLine.quantity,
216
+ availableQuantity: requestLine.quantity - remaining,
217
+ missingQuantity: remaining,
218
+ status: remaining === 0 ? "fulfillable" : "insufficient",
219
+ allocations,
220
+ };
221
+ });
222
+
223
+ return {
224
+ status: lines.every((line) => line.status === "fulfillable") ? "fulfillable" : "insufficient",
225
+ locationId,
226
+ locationCode,
227
+ lines,
228
+ };
229
+ }
230
+
231
+ function isUsable(line: InventoryStockLine, requestedAt: string, minShelfLifeDays: number): boolean {
232
+ if (!line.fulfillable || line.locationStatus !== "active") return false;
233
+ if (line.lotStatus && line.lotStatus !== "available") return false;
234
+ if (line.expiresAt && Date.parse(line.expiresAt) <= minAcceptableExpiry(requestedAt, minShelfLifeDays)) {
235
+ return false;
236
+ }
237
+ return availableNow(line) > 0;
238
+ }
239
+
240
+ function minAcceptableExpiry(requestedAt: string, minShelfLifeDays: number): number {
241
+ return Date.parse(requestedAt) + minShelfLifeDays * 24 * 60 * 60 * 1000;
242
+ }
243
+
244
+ function fefoSort(a: InventoryStockLine, b: InventoryStockLine): number {
245
+ const aExpiry = a.expiresAt ? Date.parse(a.expiresAt) : Number.POSITIVE_INFINITY;
246
+ const bExpiry = b.expiresAt ? Date.parse(b.expiresAt) : Number.POSITIVE_INFINITY;
247
+ if (aExpiry !== bExpiry) return aExpiry - bExpiry;
248
+ return a.locationCode.localeCompare(b.locationCode) || (a.lotCode ?? "").localeCompare(b.lotCode ?? "");
249
+ }
@@ -0,0 +1,2 @@
1
+ /** @beta */
2
+ export * from "./atp.js";
@@ -0,0 +1,91 @@
1
+ import { z } from "zod";
2
+
3
+ /** @beta */
4
+ export type MarketingResearchJson =
5
+ | string
6
+ | number
7
+ | boolean
8
+ | null
9
+ | { [key: string]: MarketingResearchJson | undefined }
10
+ | MarketingResearchJson[];
11
+
12
+ /** @beta */
13
+ export const marketingResearchJsonSchema = z.custom<MarketingResearchJson>(
14
+ isMarketingResearchJson,
15
+ { message: "Expected JSON value" },
16
+ );
17
+
18
+ /** @beta */
19
+ export const researchSurveyTypeSchema = z.enum(["producer", "consumer"]);
20
+
21
+ /** @beta */
22
+ export const adminSurveyResponsesRequestSchema = z
23
+ .object({
24
+ surveyType: researchSurveyTypeSchema,
25
+ page: z.coerce.number().int().nonnegative().default(0),
26
+ pageSize: z.coerce.number().int().min(1).max(100).optional(),
27
+ limit: z.coerce.number().int().min(1).max(500).optional(),
28
+ })
29
+ .transform(({ limit, pageSize, ...request }) => ({
30
+ ...request,
31
+ pageSize: pageSize ?? Math.min(limit ?? 50, 100),
32
+ }));
33
+
34
+ /** @beta */
35
+ export const adminSurveyResponseRowSchema = z.object({
36
+ id: z.string().min(1),
37
+ created_at: z.string().min(1),
38
+ response_data: marketingResearchJsonSchema,
39
+ });
40
+
41
+ /** @beta */
42
+ export const adminSurveyResponsesResponseSchema = z.object({
43
+ rows: z.array(adminSurveyResponseRowSchema),
44
+ totalCount: z.number().int().nonnegative(),
45
+ page: z.number().int().nonnegative(),
46
+ pageSize: z.number().int().positive(),
47
+ });
48
+
49
+ /** @beta */
50
+ export const researchSurveySubmitRequestSchema = z
51
+ .object({
52
+ surveyType: researchSurveyTypeSchema,
53
+ responseData: z.record(z.string(), marketingResearchJsonSchema),
54
+ })
55
+ .refine((request) => Object.keys(request.responseData).length > 0, {
56
+ message: "responseData must not be empty",
57
+ path: ["responseData"],
58
+ });
59
+
60
+ /** @beta */
61
+ export const researchSurveySubmitResponseSchema = z.object({
62
+ ok: z.boolean(),
63
+ id: z.string().min(1).optional(),
64
+ error: z.string().optional(),
65
+ email_skipped: z.string().optional(),
66
+ });
67
+
68
+ /** @beta */
69
+ export type ResearchSurveyType = z.infer<typeof researchSurveyTypeSchema>;
70
+ /** @beta */
71
+ export type AdminSurveyResponsesRequest = z.infer<typeof adminSurveyResponsesRequestSchema>;
72
+ /** @beta */
73
+ export type AdminSurveyResponseRow = z.infer<typeof adminSurveyResponseRowSchema>;
74
+ /** @beta */
75
+ export type AdminSurveyResponsesResponse = z.infer<typeof adminSurveyResponsesResponseSchema>;
76
+ /** @beta */
77
+ export type ResearchSurveySubmitRequest = z.infer<typeof researchSurveySubmitRequestSchema>;
78
+ /** @beta */
79
+ export type ResearchSurveySubmitResponse = z.infer<typeof researchSurveySubmitResponseSchema>;
80
+
81
+ function isMarketingResearchJson(value: unknown): value is MarketingResearchJson {
82
+ if (value === null) return true;
83
+ if (typeof value === "string" || typeof value === "boolean") return true;
84
+ if (typeof value === "number") return Number.isFinite(value);
85
+ if (Array.isArray(value)) return value.every(isMarketingResearchJson);
86
+ if (typeof value !== "object") return false;
87
+
88
+ return Object.values(value as Record<string, unknown>).every(
89
+ (entry) => entry === undefined || isMarketingResearchJson(entry),
90
+ );
91
+ }
@@ -0,0 +1,4 @@
1
+ /** @beta */
2
+ export * from "./contracts.js";
3
+ /** @beta */
4
+ export * from "./ports.js";