@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,26 @@
1
+ import type {
2
+ AdminSurveyResponsesRequest,
3
+ AdminSurveyResponsesResponse,
4
+ ResearchSurveySubmitRequest,
5
+ ResearchSurveySubmitResponse,
6
+ } from "./contracts.js";
7
+
8
+ /** @beta */
9
+ export interface MarketingResearchSurveyResponsesReadPort {
10
+ getAdminSurveyResponses(
11
+ request: AdminSurveyResponsesRequest,
12
+ ): Promise<AdminSurveyResponsesResponse>;
13
+ }
14
+
15
+ /** @beta */
16
+ export interface MarketingResearchSurveyRequestHeaders {
17
+ forEach(callbackfn: (value: string, key: string) => void): void;
18
+ }
19
+
20
+ /** @beta */
21
+ export interface MarketingResearchSurveySubmitPort {
22
+ submitSurveyResponse(
23
+ request: ResearchSurveySubmitRequest,
24
+ context?: { headers?: MarketingResearchSurveyRequestHeaders },
25
+ ): Promise<ResearchSurveySubmitResponse>;
26
+ }
@@ -0,0 +1,4 @@
1
+ /** @beta */
2
+ export * from "./status.js";
3
+ /** @beta */
4
+ export * from "./ports.js";
@@ -0,0 +1,18 @@
1
+ import type {
2
+ PartnersB2BInquiryStatusUpdateRequest,
3
+ PartnersB2BInquiryStatusUpdateResponse,
4
+ } from "./status.js";
5
+
6
+ /**
7
+ * Persistence seam for advancing a B2B inquiry's pipeline status. The host
8
+ * application supplies the adapter (e.g. a DB-backed store, a CRM bridge); the
9
+ * core only owns this contract, not where inquiries are stored. Implementations
10
+ * should reject (throw) when the inquiry does not exist or the transition is
11
+ * not allowed, rather than resolving with a non-updated result.
12
+ */
13
+ /** @beta */
14
+ export interface PartnersB2BInquiryStatusUpdatePort {
15
+ updateB2BInquiryStatus(
16
+ request: PartnersB2BInquiryStatusUpdateRequest,
17
+ ): Promise<PartnersB2BInquiryStatusUpdateResponse>;
18
+ }
@@ -0,0 +1,49 @@
1
+ import { z } from "zod";
2
+
3
+ /**
4
+ * Lifecycle stage of a B2B / partner inquiry as it moves through a sales
5
+ * pipeline. Neutral, provider-agnostic vocabulary:
6
+ * - `new` — just received, not yet triaged;
7
+ * - `contacted` — outreach made, awaiting response;
8
+ * - `qualified` — a real, in-scope opportunity;
9
+ * - `disqualified` — not a fit (out of scope / spam / no intent);
10
+ * - `closed_won` — converted;
11
+ * - `closed_lost` — did not convert.
12
+ * The inquiry payload itself (contact details, CRM ids, PII) is NOT part of the
13
+ * core contract; it lives in the host application's downstream adapter.
14
+ */
15
+ /** @beta */
16
+ export const partnersB2BInquiryStatusSchema = z.enum([
17
+ "new",
18
+ "contacted",
19
+ "qualified",
20
+ "disqualified",
21
+ "closed_won",
22
+ "closed_lost",
23
+ ]);
24
+
25
+ /** Request to move one inquiry (`id`) to a new pipeline `status`. */
26
+ /** @beta */
27
+ export const partnersB2BInquiryStatusUpdateRequestSchema = z.object({
28
+ id: z.string().min(1),
29
+ status: partnersB2BInquiryStatusSchema,
30
+ });
31
+
32
+ /** Successful status-update acknowledgement. `updated` is always `true`; a
33
+ * failed update is surfaced as a rejected promise / error by the port, not as
34
+ * `{ updated: false }`. */
35
+ /** @beta */
36
+ export const partnersB2BInquiryStatusUpdateResponseSchema = z.object({
37
+ updated: z.literal(true),
38
+ });
39
+
40
+ /** @beta */
41
+ export type PartnersB2BInquiryStatus = z.infer<typeof partnersB2BInquiryStatusSchema>;
42
+ /** @beta */
43
+ export type PartnersB2BInquiryStatusUpdateRequest = z.infer<
44
+ typeof partnersB2BInquiryStatusUpdateRequestSchema
45
+ >;
46
+ /** @beta */
47
+ export type PartnersB2BInquiryStatusUpdateResponse = z.infer<
48
+ typeof partnersB2BInquiryStatusUpdateResponseSchema
49
+ >;
@@ -0,0 +1,93 @@
1
+ import type { PaymentAttemptStatus } from "./paymentControlTypes.js";
2
+ import type { PaymentFailureHint } from "./paymentFailureTaxonomyContracts.js";
3
+
4
+ /** @beta */
5
+ export const PAYMENT_EXECUTION_MODES = ["one_time", "subscription_cycle"] as const;
6
+ /** @beta */
7
+ export type PaymentExecutionMode = (typeof PAYMENT_EXECUTION_MODES)[number];
8
+
9
+ /** @beta */
10
+ export const PAYMENT_EXECUTION_ATTEMPT_STATUSES = [
11
+ "sent_to_provider",
12
+ "requires_action",
13
+ "processing",
14
+ ] as const satisfies readonly PaymentAttemptStatus[];
15
+ /** @beta */
16
+ export type PaymentExecutionAttemptStatus = Extract<
17
+ PaymentAttemptStatus,
18
+ (typeof PAYMENT_EXECUTION_ATTEMPT_STATUSES)[number]
19
+ >;
20
+
21
+ /** @beta */
22
+ export interface PaymentExecutionBaseInput {
23
+ idempotencyKey: string;
24
+ providerIdempotencyKey: string;
25
+ providerRequestFingerprint: string;
26
+ paymentIntentId: string;
27
+ amountMinor: number;
28
+ currency: string;
29
+ mode: PaymentExecutionMode;
30
+ }
31
+
32
+ /**
33
+ * A provider that refused the attempt inside the execution call itself, rather
34
+ * than through a later callback.
35
+ *
36
+ * Execution adapters cannot express this as an attempt status: only the control
37
+ * plane performs terminal transitions, so {@link PaymentExecutionAttemptStatus}
38
+ * is deliberately non-terminal. The adapter therefore reports the refusal as a
39
+ * fact and lets the caller finalize the attempt.
40
+ *
41
+ * Reporting it matters because some providers offer no second chance to learn of
42
+ * the failure: where a provider's callback fires only for successful payments, a
43
+ * synchronous decline that is not surfaced here is never reported at all, and
44
+ * the attempt hangs until it expires.
45
+ */
46
+ /** @beta */
47
+ export interface PaymentExecutionProviderDecline {
48
+ /** Provider-native code, for correlation. Low-cardinality; never a free-form message. */
49
+ code: string;
50
+ /** True when the provider states the payer's bank cannot register a reusable mandate. */
51
+ mandateUnsupported: boolean;
52
+ /**
53
+ * The finer refusal reason the payer's bank returned, when the provider passes
54
+ * one through beside {@link code}. Optional because most providers report a
55
+ * single code; still a code, never a message.
56
+ */
57
+ declineCode?: string;
58
+ /**
59
+ * The provider's machine-readable recommendation for what a retry should do
60
+ * (for example: do not retry this instrument again). Optional and advisory —
61
+ * nothing in the decline path branches on it yet; it is carried so a later
62
+ * retry policy is not forced to re-query the provider for evidence that only
63
+ * existed at refusal time.
64
+ */
65
+ adviceCode?: string;
66
+ /**
67
+ * What the adapter is willing to ASSERT about this refusal, already translated
68
+ * out of its own code vocabulary into {@link PaymentFailureHint}.
69
+ *
70
+ * Carried on the decline so a classifier can read a refusal without learning
71
+ * which adapter produced it: the acquirer's codes stop at the adapter that
72
+ * owns them, and only the neutral reading crosses into the control plane.
73
+ * Absent when the adapter has no opinion, which classifies as `indeterminate`
74
+ * rather than as a guess.
75
+ */
76
+ neutralReasonHints?: readonly PaymentFailureHint[];
77
+ }
78
+
79
+ /** @beta */
80
+ export interface PaymentExecutionBaseResult {
81
+ provider: string;
82
+ providerAttemptId: string | null;
83
+ providerSessionId: string | null;
84
+ attemptStatus: PaymentExecutionAttemptStatus;
85
+ nextActionKind: string | null;
86
+ /**
87
+ * Set when the provider refused synchronously. `null` for the normal path,
88
+ * where the outcome arrives later via callback or polling.
89
+ */
90
+ providerDecline?: PaymentExecutionProviderDecline | null;
91
+ requestPayload: Record<string, unknown>;
92
+ responsePayload: Record<string, unknown>;
93
+ }
@@ -0,0 +1,128 @@
1
+ /** @beta */
2
+ export {
3
+ LIVE_ATTEMPT_STATUSES,
4
+ PAYMENT_ATTEMPT_STATUSES,
5
+ PAYMENT_CONTROL_EVENT_TYPES,
6
+ PAYMENT_INTENT_STATUSES,
7
+ PAYMENT_TARGET_KINDS,
8
+ RETRYABLE_ATTEMPT_STATUSES,
9
+ isLiveAttemptStatus,
10
+ isRetryableAttemptStatus,
11
+ providerAckedButNeverConfirmed,
12
+ } from "./paymentControlTypes.js";
13
+ /** @beta */
14
+ export {
15
+ PAYMENT_EXECUTION_ATTEMPT_STATUSES,
16
+ PAYMENT_EXECUTION_MODES,
17
+ } from "./executionBaseContracts.js";
18
+ /** @beta */
19
+ export type {
20
+ CanonicalPaymentControlEvent,
21
+ PaymentAttemptAggregate,
22
+ PaymentAttemptStatus,
23
+ PaymentControlDecision,
24
+ PaymentControlDecisionKind,
25
+ PaymentControlEventType,
26
+ PaymentControlFailureReason,
27
+ PaymentControlResult,
28
+ PaymentIntentAggregate,
29
+ PaymentIntentStatus,
30
+ PaymentTargetKind,
31
+ } from "./paymentControlTypes.js";
32
+ /** @beta */
33
+ export type {
34
+ PaymentExecutionAttemptStatus,
35
+ PaymentExecutionBaseInput,
36
+ PaymentExecutionBaseResult,
37
+ PaymentExecutionProviderDecline,
38
+ PaymentExecutionMode,
39
+ } from "./executionBaseContracts.js";
40
+ /** @beta */
41
+ export {
42
+ PAYMENT_FAILURE_CLASSES,
43
+ PAYMENT_FAILURE_CUSTOMER_CAUSES,
44
+ PAYMENT_FAILURE_HINTS,
45
+ PAYMENT_RETRY_ADVICE_CODES,
46
+ customerCauseForFailureClass,
47
+ failureClassDecision,
48
+ } from "./paymentFailureTaxonomyContracts.js";
49
+ /** @beta */
50
+ export type {
51
+ PaymentFailureClass,
52
+ PaymentFailureCustomerCause,
53
+ PaymentFailureDecision,
54
+ PaymentFailureEvidence,
55
+ PaymentFailureHint,
56
+ PaymentRetryAdviceCode,
57
+ PaymentRetryProfile,
58
+ } from "./paymentFailureTaxonomyContracts.js";
59
+ /** @beta */
60
+ export {
61
+ PAYMENT_FAILURE_CLASSIFICATION_SOURCES,
62
+ SCHEME_RETRY_ATTEMPT_CEILING,
63
+ SCHEME_RETRY_WINDOW_DAYS,
64
+ classifyPaymentFailure,
65
+ isPaymentRetryAdviceCode,
66
+ withinSchemeRetryCeiling,
67
+ } from "./paymentFailureTaxonomy.js";
68
+ /** @beta */
69
+ export type {
70
+ PaymentFailureClassification,
71
+ PaymentFailureClassificationOptions,
72
+ PaymentFailureClassificationSource,
73
+ } from "./paymentFailureTaxonomy.js";
74
+ /** @beta */
75
+ export {
76
+ applyProviderPaymentEvent,
77
+ canTransitionPaymentAttempt,
78
+ canTransitionPaymentIntent,
79
+ transitionPaymentAttempt,
80
+ transitionPaymentIntent,
81
+ } from "./paymentStateMachine.js";
82
+ /** @beta */
83
+ export {
84
+ buildProviderAttemptIdentity,
85
+ isProviderAttemptReplaySafe,
86
+ } from "./providerAttemptIdempotency.js";
87
+ /** @beta */
88
+ export type {
89
+ ProviderAttemptIdentity,
90
+ ProviderAttemptIdentityInput,
91
+ } from "./providerAttemptIdempotency.js";
92
+ /** @beta */
93
+ export {
94
+ PAYMENT_METHOD_LIFECYCLE_EVENT_KINDS,
95
+ endsStoredMethodUsability,
96
+ expiryInstantFromMonthYear,
97
+ isPaymentMethodLifecycleEventKind,
98
+ } from "./methodLifecycle.js";
99
+ /** @beta */
100
+ export type {
101
+ PaymentMethodLifecycleEvent,
102
+ PaymentMethodLifecycleEventKind,
103
+ PaymentMethodReplacementFacts,
104
+ } from "./methodLifecycle.js";
105
+ /** @beta */
106
+ export type {
107
+ PayerContextRequirements,
108
+ PaymentMethodCaptureFlow,
109
+ PaymentMethodCaptureHandoff,
110
+ PaymentMethodHealthExpectations,
111
+ PaymentProviderCapabilityDescriptor,
112
+ PaymentProviderCapabilityRegistry,
113
+ StoredMandateSnapshot,
114
+ UnattendedChargeBlockReason,
115
+ UnattendedMandateAssessment,
116
+ } from "./providerCapability.js";
117
+ /** @beta */
118
+ export { PAYMENT_RECOVERY_CAUSES, PAYMENT_RECOVERY_ACTIONS } from "./paymentRecoveryContracts.js";
119
+ /** @beta */
120
+ export type {
121
+ PaymentRecoveryCause, PaymentRecoveryAction, PaymentRecoveryOperation,
122
+ PaymentRecoveryMethod, PaymentRecoveryAdvice, PaymentRecoveryEvidence,
123
+ PaymentRecoveryCandidate, PaymentRecoveryNormalizer,
124
+ } from "./paymentRecoveryContracts.js";
125
+ /** @beta */
126
+ export { derivePaymentRecoveryGuidance, availablePaymentRecoveryActions } from "./paymentRecovery.js";
127
+ /** @beta */
128
+ export type { PaymentRecoveryAttempt, PaymentRecoveryGuidance } from "./paymentRecovery.js";
@@ -0,0 +1,130 @@
1
+ /**
2
+ * Neutral payment-method lifecycle vocabulary.
3
+ *
4
+ * A stored consent does not only come into existence. It is rotated, revoked
5
+ * and suspended, and it expires — and every rail announces those transitions in
6
+ * its own words. This module is the vocabulary those announcements are
7
+ * translated INTO, so the engine that reacts (deactivating a dead method,
8
+ * warning a payer before a renewal that can no longer be charged) never has to
9
+ * learn a rail's event names.
10
+ *
11
+ * Types and pure functions only: nothing here reads a clock, a network or a
12
+ * database, and no rail is named.
13
+ */
14
+
15
+ /**
16
+ * Every transition a rail may report about a method it holds on file.
17
+ *
18
+ * `method_suspended` is declared but published by no rail in this deployment.
19
+ * It is here because the consuming branch is exhaustive over this list: a rail
20
+ * that starts emitting a suspension must be handled, never silently ignored.
21
+ */
22
+ export const PAYMENT_METHOD_LIFECYCLE_EVENT_KINDS = [
23
+ "method_registered",
24
+ "method_updated",
25
+ "method_revoked",
26
+ "method_expired",
27
+ "method_suspended",
28
+ ] as const;
29
+
30
+ export type PaymentMethodLifecycleEventKind =
31
+ (typeof PAYMENT_METHOD_LIFECYCLE_EVENT_KINDS)[number];
32
+
33
+ /**
34
+ * What the rail reports about the method AFTER the transition.
35
+ *
36
+ * Deliberately three fields and no more: the label a payer recognises the
37
+ * method by, the trailing digits shown beside it, and the instant it stops
38
+ * being chargeable. ⛔ The full instrument number is never part of this
39
+ * contract, and an adapter that put one here would be widening a surface this
40
+ * type exists to keep narrow.
41
+ *
42
+ * Every field is independently nullable because rails report them
43
+ * independently: an alias rail may carry an expiry and no digits at all.
44
+ */
45
+ export interface PaymentMethodReplacementFacts {
46
+ /** Scheme or rail label the payer recognises, e.g. a card network name. */
47
+ schemeLabel: string | null;
48
+ /** Trailing digits of the instrument, never more than the payer is shown. */
49
+ lastDigits: string | null;
50
+ /** ISO-8601 instant after which the method can no longer be charged. */
51
+ expiresAt: string | null;
52
+ }
53
+
54
+ /**
55
+ * One rail transition, expressed neutrally.
56
+ *
57
+ * `providerKind` and `providerMethodRef` are opaque identity, not a branch
58
+ * condition: together they address exactly one stored method. A consumer that
59
+ * compares `providerKind` against a known name re-creates the coupling this
60
+ * contract removes.
61
+ */
62
+ export interface PaymentMethodLifecycleEvent {
63
+ kind: PaymentMethodLifecycleEventKind;
64
+ /** Registry key of the rail that reported this, used only for addressing. */
65
+ providerKind: string;
66
+ /** The rail's own reference for the stored method this transition is about. */
67
+ providerMethodRef: string;
68
+ /** The rail's identifier for this delivery, used for replay-safe writes. */
69
+ providerEventId: string;
70
+ /** ISO-8601 instant the rail reported the transition at. */
71
+ occurredAt: string;
72
+ /** Fresh facts the rail published with the transition, when it published any. */
73
+ replacement?: PaymentMethodReplacementFacts | null;
74
+ }
75
+
76
+ /**
77
+ * Whether this transition means the stored method can no longer back a charge.
78
+ *
79
+ * The single place that answers it. ⛔ A rotation (`method_updated`) is NOT a
80
+ * death: the same consent continues under fresh details, and treating it as one
81
+ * would strip a healthy subscription of the method it renews on.
82
+ */
83
+ export function endsStoredMethodUsability(
84
+ kind: PaymentMethodLifecycleEventKind,
85
+ ): boolean {
86
+ return kind === "method_revoked" || kind === "method_expired" || kind === "method_suspended";
87
+ }
88
+
89
+ export function isPaymentMethodLifecycleEventKind(
90
+ value: unknown,
91
+ ): value is PaymentMethodLifecycleEventKind {
92
+ return typeof value === "string"
93
+ && (PAYMENT_METHOD_LIFECYCLE_EVENT_KINDS as readonly string[]).includes(value);
94
+ }
95
+
96
+ /**
97
+ * The instant a month-and-year validity actually runs out.
98
+ *
99
+ * Convention, pinned here once so no adapter has to re-decide it: an
100
+ * instrument valid "through 06/2027" is chargeable for the whole of that month
101
+ * and stops at the LAST instant of it, in UTC — `2027-06-30T23:59:59.999Z`. The
102
+ * common alternative, the first instant of the month, would retire a method up
103
+ * to 31 days before the payer's own statement says it dies.
104
+ *
105
+ * UTC rather than a local zone on purpose: the value is compared against
106
+ * `now()` by storage and by health reads, both of which are UTC, and the
107
+ * one-day ambiguity a local zone would introduce is not worth a timezone
108
+ * dependency in a kernel.
109
+ *
110
+ * Returns null for anything that is not a plainly valid month and four-digit
111
+ * year, so a malformed rail payload yields "no expiry known" rather than a
112
+ * fabricated date.
113
+ */
114
+ export function expiryInstantFromMonthYear(
115
+ month: number | null | undefined,
116
+ year: number | null | undefined,
117
+ ): string | null {
118
+ if (!Number.isInteger(month) || !Number.isInteger(year)) return null;
119
+ const monthValue = month as number;
120
+ const yearValue = year as number;
121
+ if (monthValue < 1 || monthValue > 12) return null;
122
+ // Below 1000 the platform's own two-digit-year remapping would silently turn
123
+ // 27 into 1927; a rail that abbreviates the year is refused, not guessed at.
124
+ if (yearValue < 1000 || yearValue > 9999) return null;
125
+ // Month is 1-based here and 0-based in the platform date constructor, so
126
+ // passing it unchanged addresses the FIRST instant of the following month;
127
+ // one millisecond earlier is the last instant of the expiry month itself.
128
+ const firstInstantAfter = Date.UTC(yearValue, monthValue, 1);
129
+ return new Date(firstInstantAfter - 1).toISOString();
130
+ }
@@ -0,0 +1,197 @@
1
+ /** @beta */
2
+ export const PAYMENT_TARGET_KINDS = ["one_time_order", "subscription_cycle"] as const;
3
+ /** @beta */
4
+ export type PaymentTargetKind = (typeof PAYMENT_TARGET_KINDS)[number];
5
+
6
+ /** @beta */
7
+ export const PAYMENT_INTENT_STATUSES = [
8
+ "created",
9
+ "requires_action",
10
+ "processing",
11
+ "succeeded",
12
+ "failed",
13
+ "expired",
14
+ "cancelled",
15
+ "refunded",
16
+ "partially_refunded",
17
+ "disputed",
18
+ ] as const;
19
+ /** @beta */
20
+ export type PaymentIntentStatus = (typeof PAYMENT_INTENT_STATUSES)[number];
21
+
22
+ /** @beta */
23
+ export const PAYMENT_ATTEMPT_STATUSES = [
24
+ "created",
25
+ "blocked_preflight",
26
+ "sent_to_provider",
27
+ "requires_action",
28
+ "processing",
29
+ "succeeded",
30
+ "failed",
31
+ "cancelled",
32
+ "expired",
33
+ ] as const;
34
+ /** @beta */
35
+ export type PaymentAttemptStatus = (typeof PAYMENT_ATTEMPT_STATUSES)[number];
36
+
37
+ /**
38
+ * The attempt statuses that prove no provider call can still be outstanding, and
39
+ * so allow a fresh attempt against the same intent.
40
+ *
41
+ * This is the admission gate's own set, not a convenience list: the durable
42
+ * gate is written in SQL as `status NOT IN (...)` over exactly these four values,
43
+ * at three call sites, and `src/domains/payment/payment.test.ts` pins all three
44
+ * against this constant. The gate is deliberately fail-closed — every status
45
+ * outside this set is treated as "a call may exist" even when it is terminal —
46
+ * because admitting a second call against a live attempt can take the payer's
47
+ * money twice, while refusing one only delays a retry.
48
+ *
49
+ * @beta
50
+ */
51
+ export const RETRYABLE_ATTEMPT_STATUSES = [
52
+ "blocked_preflight",
53
+ "failed",
54
+ "cancelled",
55
+ "expired",
56
+ ] as const satisfies readonly PaymentAttemptStatus[];
57
+
58
+ /**
59
+ * The attempt statuses that describe an outcome still in the air: the request
60
+ * exists, and neither success nor refusal is known yet.
61
+ *
62
+ * NOT the complement of {@link RETRYABLE_ATTEMPT_STATUSES}. The complement also
63
+ * contains `succeeded`, which is settled rather than live, so the two predicates
64
+ * relate as `!isRetryableAttemptStatus(s) === isLiveAttemptStatus(s) || s ===
65
+ * "succeeded"` — an identity the canon test pins, because reasoning about it
66
+ * from the two lists alone has already gone wrong once in this repository.
67
+ *
68
+ * @beta
69
+ */
70
+ export const LIVE_ATTEMPT_STATUSES = [
71
+ "created",
72
+ "sent_to_provider",
73
+ "requires_action",
74
+ "processing",
75
+ ] as const satisfies readonly PaymentAttemptStatus[];
76
+
77
+ /**
78
+ * Whether a fresh attempt may be admitted against the intent this attempt
79
+ * belongs to. See {@link RETRYABLE_ATTEMPT_STATUSES}.
80
+ *
81
+ * @beta
82
+ */
83
+ export function isRetryableAttemptStatus(status: string): boolean {
84
+ return (RETRYABLE_ATTEMPT_STATUSES as readonly string[]).includes(status);
85
+ }
86
+
87
+ /**
88
+ * Whether this attempt's outcome is still unresolved — the row a status poller,
89
+ * a reconciliation sweep or a stuck-attempt watchdog is looking for. See
90
+ * {@link LIVE_ATTEMPT_STATUSES}.
91
+ *
92
+ * @beta
93
+ */
94
+ export function isLiveAttemptStatus(status: string): boolean {
95
+ return (LIVE_ATTEMPT_STATUSES as readonly string[]).includes(status);
96
+ }
97
+
98
+ /**
99
+ * Whether the request demonstrably reached the provider and nothing came back.
100
+ *
101
+ * The single status that says so is `sent_to_provider`, and it exists because the
102
+ * alternatives both lie. `created` claims the provider was never contacted, so a
103
+ * caller is free to contact it again. `processing` claims money is moving, which
104
+ * is what a payer is then told — and that exact claim was made about intents no
105
+ * issuer had ever been asked about (recorded in the deployment payment-status canon).
106
+ *
107
+ * Reaching for this predicate instead of comparing the string is the point: the
108
+ * mapping that produced the wrong verdict lived in an adapter that could not see
109
+ * the reasoning, which sat unexported in a file beside it.
110
+ *
111
+ * @beta
112
+ */
113
+ export function providerAckedButNeverConfirmed(status: string): boolean {
114
+ return status === "sent_to_provider";
115
+ }
116
+
117
+ /** @beta */
118
+ export const PAYMENT_CONTROL_EVENT_TYPES = [
119
+ "payment.requires_action",
120
+ "payment.succeeded",
121
+ "payment.failed",
122
+ "payment.refunded",
123
+ "payment.disputed",
124
+ ] as const;
125
+ /** @beta */
126
+ export type PaymentControlEventType = (typeof PAYMENT_CONTROL_EVENT_TYPES)[number];
127
+
128
+ /** @beta */
129
+ export interface PaymentIntentAggregate {
130
+ id: string;
131
+ targetKind: PaymentTargetKind;
132
+ targetId: string;
133
+ status: PaymentIntentStatus;
134
+ amountMinor: number;
135
+ currency: string;
136
+ activeAttemptId: string | null;
137
+ updatedAt: string;
138
+ providerPaymentId?: string | null;
139
+ failureReason?: string | null;
140
+ }
141
+
142
+ /** @beta */
143
+ export interface PaymentAttemptAggregate {
144
+ id: string;
145
+ intentId: string;
146
+ status: PaymentAttemptStatus;
147
+ provider: string;
148
+ providerAttemptId: string | null;
149
+ amountMinor: number;
150
+ currency: string;
151
+ idempotencyKey: string;
152
+ updatedAt: string;
153
+ nextActionKind?: string | null;
154
+ failureReason?: string | null;
155
+ }
156
+
157
+ /** @beta */
158
+ export interface CanonicalPaymentControlEvent {
159
+ provider: string;
160
+ providerEventId: string;
161
+ eventType: PaymentControlEventType;
162
+ providerPaymentId: string;
163
+ amountMinor?: number;
164
+ currency?: string;
165
+ occurredAt: string;
166
+ failureReason?: string;
167
+ nextActionKind?: string;
168
+ rawPayload: Record<string, unknown>;
169
+ }
170
+
171
+ /** @beta */
172
+ export type PaymentControlDecisionKind =
173
+ | "state_changed"
174
+ | "noop"
175
+ | "late_success_recovered";
176
+
177
+ /** @beta */
178
+ export interface PaymentControlDecision {
179
+ kind: PaymentControlDecisionKind;
180
+ reason: string;
181
+ intent: PaymentIntentAggregate;
182
+ attempt: PaymentAttemptAggregate;
183
+ }
184
+
185
+ /** @beta */
186
+ export type PaymentControlFailureReason =
187
+ | "amount_mismatch"
188
+ | "currency_mismatch"
189
+ | "invalid_transition"
190
+ | "refund_requires_success"
191
+ | "terminal_intent"
192
+ | "wrong_attempt";
193
+
194
+ /** @beta */
195
+ export type PaymentControlResult<T> =
196
+ | { ok: true; value: T }
197
+ | { ok: false; reason: PaymentControlFailureReason; message: string };