@openlup/core 0.0.0 → 0.7.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 +225 -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 +85 -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 +167 -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 +95 -0
  113. package/dist/subscription/cycleHardening.js +118 -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 +150 -0
  119. package/dist/subscription/subscriptionEngineLifecycle.js +433 -0
  120. package/dist/subscription/subscriptionEnginePayment.d.ts +25 -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 +208 -0
  139. package/src/company-identity/index.ts +28 -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 +174 -0
  185. package/src/subscription/index.ts +14 -0
  186. package/src/subscription/subscriptionEngineCore.ts +181 -0
  187. package/src/subscription/subscriptionEngineLifecycle.ts +574 -0
  188. package/src/subscription/subscriptionEnginePayment.ts +179 -0
  189. package/src/subscription/subscriptionEngineTypes.ts +114 -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,118 @@
1
+ const SEVERITY_WEIGHT = {
2
+ low: 1,
3
+ medium: 2,
4
+ high: 3,
5
+ critical: 4,
6
+ };
7
+ /** @beta */
8
+ export function collectRiskRuleMatches(input) {
9
+ const matches = [];
10
+ const add = (match) => matches.push(match);
11
+ const { checkout, payment, signals } = input;
12
+ if (signals.exactBlocklistMatches.length > 0) {
13
+ add({
14
+ code: "exact_blocklist_match",
15
+ score: 100,
16
+ severity: "critical",
17
+ evidence: { matchCount: signals.exactBlocklistMatches.length },
18
+ });
19
+ }
20
+ if (signals.recentCheckoutAttemptsByIdentity >= 3) {
21
+ add({
22
+ code: "velocity_checkout_attempts",
23
+ score: 35,
24
+ severity: "medium",
25
+ evidence: { attempts: signals.recentCheckoutAttemptsByIdentity, window: "1h" },
26
+ });
27
+ }
28
+ if (signals.distinctEmailsByIp24h >= 3) {
29
+ add({
30
+ code: "velocity_email_cluster",
31
+ score: 45,
32
+ severity: "high",
33
+ evidence: { distinctEmails: signals.distinctEmailsByIp24h, window: "24h" },
34
+ });
35
+ }
36
+ if (signals.discountedPaidOrders30d >= 2) {
37
+ add({
38
+ code: "velocity_discount_reuse",
39
+ score: 40,
40
+ severity: "medium",
41
+ evidence: { discountedPaidOrders: signals.discountedPaidOrders30d, window: "30d" },
42
+ });
43
+ }
44
+ if (signals.clientsSharingPaymentMethod7d >= 2) {
45
+ add({
46
+ code: "duplicate_payment_method",
47
+ score: 50,
48
+ severity: "high",
49
+ evidence: { clients: signals.clientsSharingPaymentMethod7d, window: "7d" },
50
+ });
51
+ }
52
+ if (signals.priorDisputes > 0) {
53
+ add({
54
+ code: "prior_dispute",
55
+ score: 80,
56
+ severity: "critical",
57
+ evidence: { disputes: signals.priorDisputes },
58
+ });
59
+ }
60
+ if (checkout?.checkoutKind === "subscription_initial" && checkout.promoCodeCount > 0 && signals.discountedPaidOrders30d > 0) {
61
+ add({
62
+ code: "subscription_promo_reuse",
63
+ score: 45,
64
+ severity: "high",
65
+ evidence: { promoCodeCount: checkout.promoCodeCount, priorDiscounts: signals.discountedPaidOrders30d },
66
+ });
67
+ }
68
+ if (checkout?.bundleDiscountApplied && signals.discountedPaidOrders30d > 0) {
69
+ add({
70
+ code: "bundle_discount_review",
71
+ score: 30,
72
+ severity: "medium",
73
+ evidence: { priorDiscounts: signals.discountedPaidOrders30d },
74
+ });
75
+ }
76
+ if (payment?.avsResult === "mismatch") {
77
+ add({ code: "avs_mismatch", score: 25, severity: "medium", evidence: { avsResult: "mismatch" } });
78
+ }
79
+ if (payment?.cvcResult === "mismatch") {
80
+ add({ code: "cvc_mismatch", score: 35, severity: "high", evidence: { cvcResult: "mismatch" } });
81
+ }
82
+ if (checkout && hasCountryMismatch(checkout.shippingCountry, checkout.billingCountry, checkout.ipCountry)) {
83
+ add({
84
+ code: "country_signal_mismatch",
85
+ score: 30,
86
+ severity: "medium",
87
+ evidence: {
88
+ shippingCountry: checkout.shippingCountry ?? null,
89
+ billingCountry: checkout.billingCountry ?? null,
90
+ ipCountry: checkout.ipCountry ?? null,
91
+ },
92
+ });
93
+ }
94
+ if (checkout && signals.highRiskCountries.some((country) => [checkout.shippingCountry, checkout.billingCountry, checkout.ipCountry].includes(country))) {
95
+ add({ code: "high_risk_country", score: 70, severity: "critical", evidence: { highRiskCountries: signals.highRiskCountries } });
96
+ }
97
+ if (payment?.localAmountMinor != null && payment.providerAmountMinor != null && payment.localAmountMinor !== payment.providerAmountMinor) {
98
+ add({
99
+ code: "payment_amount_mismatch",
100
+ score: 80,
101
+ severity: "critical",
102
+ evidence: { localAmountMinor: payment.localAmountMinor, providerAmountMinor: payment.providerAmountMinor },
103
+ });
104
+ }
105
+ if (payment?.localCurrency && payment.providerCurrency && payment.localCurrency !== payment.providerCurrency) {
106
+ add({
107
+ code: "payment_currency_mismatch",
108
+ score: 80,
109
+ severity: "critical",
110
+ evidence: { localCurrency: payment.localCurrency, providerCurrency: payment.providerCurrency },
111
+ });
112
+ }
113
+ return matches.sort((a, b) => b.score - a.score || SEVERITY_WEIGHT[b.severity] - SEVERITY_WEIGHT[a.severity] || a.code.localeCompare(b.code));
114
+ }
115
+ function hasCountryMismatch(...countries) {
116
+ const present = countries.filter((country) => Boolean(country));
117
+ return new Set(present).size > 1;
118
+ }
@@ -0,0 +1,65 @@
1
+ import type { RiskDecision, RiskEnforcementMode, RiskReasonCode, RiskSeverity } from "../types.js";
2
+ /** @beta */
3
+ export interface RiskCheckoutFacts {
4
+ checkoutKind: "one_time" | "subscription_initial";
5
+ totalMinor: number;
6
+ currency: string;
7
+ promoCodeCount: number;
8
+ bundleDiscountApplied: boolean;
9
+ shippingCountry?: string | null;
10
+ billingCountry?: string | null;
11
+ ipCountry?: string | null;
12
+ saveForFutureUse?: boolean;
13
+ }
14
+ /** @beta */
15
+ export interface RiskPaymentFacts {
16
+ resultStatus?: "succeeded" | "failed" | "refunded" | "partially_refunded" | "disputed" | null;
17
+ localAmountMinor?: number | null;
18
+ providerAmountMinor?: number | null;
19
+ localCurrency?: string | null;
20
+ providerCurrency?: string | null;
21
+ avsResult?: "match" | "mismatch" | "unavailable" | null;
22
+ cvcResult?: "match" | "mismatch" | "unavailable" | null;
23
+ }
24
+ /** @beta */
25
+ export interface RiskSignalFacts {
26
+ exactBlocklistMatches: Array<{
27
+ subjectKind: string;
28
+ reasonCode?: string | null;
29
+ }>;
30
+ recentCheckoutAttemptsByIdentity: number;
31
+ distinctEmailsByIp24h: number;
32
+ discountedPaidOrders30d: number;
33
+ clientsSharingPaymentMethod7d: number;
34
+ priorDisputes: number;
35
+ highRiskCountries: string[];
36
+ }
37
+ /** @beta */
38
+ export interface RiskEvaluationInput {
39
+ source: "checkout" | "paid_order" | "payment_webhook" | "admin_replay";
40
+ mode: RiskEnforcementMode;
41
+ checkout?: RiskCheckoutFacts;
42
+ payment?: RiskPaymentFacts;
43
+ signals: RiskSignalFacts;
44
+ }
45
+ /** @beta */
46
+ export interface RiskRuleMatch {
47
+ code: RiskReasonCode;
48
+ score: number;
49
+ severity: RiskSeverity;
50
+ evidence: Record<string, unknown>;
51
+ }
52
+ /** @beta */
53
+ export interface RiskEvaluationOutput {
54
+ decision: RiskDecision;
55
+ score: number;
56
+ severity: RiskSeverity;
57
+ reasonCodes: RiskReasonCode[];
58
+ matchedRules: RiskRuleMatch[];
59
+ enforcement: {
60
+ mode: RiskEnforcementMode;
61
+ applied: boolean;
62
+ checkoutBlocked: boolean;
63
+ holdRequested: boolean;
64
+ };
65
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,8 @@
1
+ /** @beta */
2
+ export * from "./types.js";
3
+ /** @beta */
4
+ export * from "./core/types.js";
5
+ /** @beta */
6
+ export * from "./core/rules.js";
7
+ /** @beta */
8
+ export * from "./core/evaluator.js";
@@ -0,0 +1,8 @@
1
+ /** @beta */
2
+ export * from "./types.js";
3
+ /** @beta */
4
+ export * from "./core/types.js";
5
+ /** @beta */
6
+ export * from "./core/rules.js";
7
+ /** @beta */
8
+ export * from "./core/evaluator.js";
@@ -0,0 +1,32 @@
1
+ /** @beta */
2
+ export declare const RISK_CONTRACT_VERSION: "risk.v1";
3
+ /** @beta */
4
+ export declare const RISK_RULESET_VERSION: "risk-rules.v1";
5
+ /** @beta */
6
+ export declare const RISK_DECISIONS: readonly ["allow", "manual_review", "block"];
7
+ /** @beta */
8
+ export declare const RISK_ENFORCEMENT_MODES: readonly ["shadow", "hold", "checkout_blocklist"];
9
+ /** @beta */
10
+ export declare const RISK_CASE_STATUSES: readonly ["open", "in_review", "approved", "blocked", "escalated", "closed"];
11
+ /** @beta */
12
+ export declare const RISK_CASE_DECISIONS: readonly ["approve", "block", "escalate", "note"];
13
+ /** @beta */
14
+ export declare const RISK_SEVERITIES: readonly ["low", "medium", "high", "critical"];
15
+ /** @beta */
16
+ export declare const RISK_SUBJECT_KINDS: readonly ["client", "email", "phone", "ip", "device", "payment_method", "shipping_address", "billing_tax_id", "identity_cluster"];
17
+ /** @beta */
18
+ export declare const RISK_REASON_CODES: readonly ["exact_blocklist_match", "velocity_checkout_attempts", "velocity_email_cluster", "velocity_discount_reuse", "duplicate_payment_method", "subscription_promo_reuse", "bundle_discount_review", "avs_mismatch", "cvc_mismatch", "country_signal_mismatch", "high_risk_country", "payment_amount_mismatch", "payment_currency_mismatch", "prior_dispute"];
19
+ /** @beta */
20
+ export type RiskDecision = (typeof RISK_DECISIONS)[number];
21
+ /** @beta */
22
+ export type RiskEnforcementMode = (typeof RISK_ENFORCEMENT_MODES)[number];
23
+ /** @beta */
24
+ export type RiskCaseStatus = (typeof RISK_CASE_STATUSES)[number];
25
+ /** @beta */
26
+ export type RiskCaseDecision = (typeof RISK_CASE_DECISIONS)[number];
27
+ /** @beta */
28
+ export type RiskSeverity = (typeof RISK_SEVERITIES)[number];
29
+ /** @beta */
30
+ export type RiskSubjectKind = (typeof RISK_SUBJECT_KINDS)[number];
31
+ /** @beta */
32
+ export type RiskReasonCode = (typeof RISK_REASON_CODES)[number];
@@ -0,0 +1,43 @@
1
+ /** @beta */
2
+ export const RISK_CONTRACT_VERSION = "risk.v1";
3
+ /** @beta */
4
+ export const RISK_RULESET_VERSION = "risk-rules.v1";
5
+ /** @beta */
6
+ export const RISK_DECISIONS = ["allow", "manual_review", "block"];
7
+ /** @beta */
8
+ export const RISK_ENFORCEMENT_MODES = ["shadow", "hold", "checkout_blocklist"];
9
+ /** @beta */
10
+ export const RISK_CASE_STATUSES = ["open", "in_review", "approved", "blocked", "escalated", "closed"];
11
+ /** @beta */
12
+ export const RISK_CASE_DECISIONS = ["approve", "block", "escalate", "note"];
13
+ /** @beta */
14
+ export const RISK_SEVERITIES = ["low", "medium", "high", "critical"];
15
+ /** @beta */
16
+ export const RISK_SUBJECT_KINDS = [
17
+ "client",
18
+ "email",
19
+ "phone",
20
+ "ip",
21
+ "device",
22
+ "payment_method",
23
+ "shipping_address",
24
+ "billing_tax_id",
25
+ "identity_cluster",
26
+ ];
27
+ /** @beta */
28
+ export const RISK_REASON_CODES = [
29
+ "exact_blocklist_match",
30
+ "velocity_checkout_attempts",
31
+ "velocity_email_cluster",
32
+ "velocity_discount_reuse",
33
+ "duplicate_payment_method",
34
+ "subscription_promo_reuse",
35
+ "bundle_discount_review",
36
+ "avs_mismatch",
37
+ "cvc_mismatch",
38
+ "country_signal_mismatch",
39
+ "high_risk_country",
40
+ "payment_amount_mismatch",
41
+ "payment_currency_mismatch",
42
+ "prior_dispute",
43
+ ];
@@ -0,0 +1,4 @@
1
+ /** @beta */
2
+ export * from "./shippingResolver.js";
3
+ /** @beta */
4
+ export * from "./types.js";
@@ -0,0 +1,4 @@
1
+ /** @beta */
2
+ export * from "./shippingResolver.js";
3
+ /** @beta */
4
+ export * from "./types.js";
@@ -0,0 +1,26 @@
1
+ import type { ResolvedShipping, ShippingResolverCart, ShippingRule } from "./types.js";
2
+ /**
3
+ * Pure shipping resolver. It picks the most specific active shipping rule for a
4
+ * cart's region, currency, and cart mode, then computes the shipping amount and
5
+ * VAT-rate snapshot. Provider selection, pickup points, labels, and fulfillment
6
+ * side effects stay outside this package.
7
+ *
8
+ * Resolution order:
9
+ * 1. Filter by active + region + currency + validity window.
10
+ * 2. Prefer an exact cart-mode rule over an `any` fallback.
11
+ * 3. Within the same mode bucket, lower priority wins.
12
+ * 4. Return null when no rule matches; the host application owns fallback UI or
13
+ * operational errors.
14
+ *
15
+ * Amount:
16
+ * - A configured free threshold makes shipping free once the cart subtotal meets
17
+ * or exceeds it.
18
+ * - Otherwise, fixed_cost_minor is used; null means this rule is always free.
19
+ *
20
+ * VAT rate snapshot:
21
+ * - `fixed` uses the rule's vat_rate_bps.
22
+ * - `inherit_main_goods` uses the most common line VAT rate.
23
+ * - `highest_in_cart` uses the max line VAT rate as a defensive accounting rule.
24
+ */
25
+ /** @beta */
26
+ export declare function resolveShipping(cart: ShippingResolverCart, rules: ReadonlyArray<ShippingRule>, now: string): ResolvedShipping | null;
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Pure shipping resolver. It picks the most specific active shipping rule for a
3
+ * cart's region, currency, and cart mode, then computes the shipping amount and
4
+ * VAT-rate snapshot. Provider selection, pickup points, labels, and fulfillment
5
+ * side effects stay outside this package.
6
+ *
7
+ * Resolution order:
8
+ * 1. Filter by active + region + currency + validity window.
9
+ * 2. Prefer an exact cart-mode rule over an `any` fallback.
10
+ * 3. Within the same mode bucket, lower priority wins.
11
+ * 4. Return null when no rule matches; the host application owns fallback UI or
12
+ * operational errors.
13
+ *
14
+ * Amount:
15
+ * - A configured free threshold makes shipping free once the cart subtotal meets
16
+ * or exceeds it.
17
+ * - Otherwise, fixed_cost_minor is used; null means this rule is always free.
18
+ *
19
+ * VAT rate snapshot:
20
+ * - `fixed` uses the rule's vat_rate_bps.
21
+ * - `inherit_main_goods` uses the most common line VAT rate.
22
+ * - `highest_in_cart` uses the max line VAT rate as a defensive accounting rule.
23
+ */
24
+ /** @beta */
25
+ export function resolveShipping(cart, rules, now) {
26
+ const eligible = rules
27
+ .filter((r) => r.active)
28
+ .filter((r) => r.region_code === cart.region_code)
29
+ .filter((r) => r.currency === cart.currency)
30
+ .filter((r) => r.valid_from <= now && (r.valid_to === null || r.valid_to >= now))
31
+ .filter((r) => r.mode_at_cart === cart.cart_mode || r.mode_at_cart === "any");
32
+ if (eligible.length === 0)
33
+ return null;
34
+ const sorted = [...eligible].sort((a, b) => {
35
+ // Exact mode match beats the cross-mode fallback.
36
+ const aSpecific = a.mode_at_cart === cart.cart_mode ? 0 : 1;
37
+ const bSpecific = b.mode_at_cart === cart.cart_mode ? 0 : 1;
38
+ if (aSpecific !== bSpecific)
39
+ return aSpecific - bSpecific;
40
+ return a.priority - b.priority;
41
+ });
42
+ const rule = sorted[0];
43
+ const amount = computeAmount(rule, cart);
44
+ const vatRate = resolveVatRate(rule, cart);
45
+ return {
46
+ amount_minor: amount,
47
+ carrier_kind: rule.carrier_kind,
48
+ delivery_estimate_days: rule.delivery_estimate_days,
49
+ applied_rule_id: rule.id,
50
+ applied_rule_name: rule.name,
51
+ vat_rate_bps: vatRate,
52
+ };
53
+ }
54
+ function computeAmount(rule, cart) {
55
+ if (rule.free_threshold_minor !== null && cart.subtotal_minor >= rule.free_threshold_minor) {
56
+ return 0;
57
+ }
58
+ return rule.fixed_cost_minor ?? 0;
59
+ }
60
+ function resolveVatRate(rule, cart) {
61
+ switch (rule.vat_rate_kind) {
62
+ case "fixed":
63
+ return rule.vat_rate_bps;
64
+ case "highest_in_cart":
65
+ return cart.line_vat_rates_bps.length === 0
66
+ ? rule.vat_rate_bps
67
+ : Math.max(...cart.line_vat_rates_bps);
68
+ case "inherit_main_goods": {
69
+ if (cart.line_vat_rates_bps.length === 0)
70
+ return rule.vat_rate_bps;
71
+ const counts = new Map();
72
+ for (const rate of cart.line_vat_rates_bps) {
73
+ counts.set(rate, (counts.get(rate) ?? 0) + 1);
74
+ }
75
+ let dominantRate = rule.vat_rate_bps;
76
+ let dominantCount = 0;
77
+ for (const [rate, count] of counts) {
78
+ if (count > dominantCount) {
79
+ dominantRate = rate;
80
+ dominantCount = count;
81
+ }
82
+ }
83
+ return dominantRate;
84
+ }
85
+ default:
86
+ return rule.vat_rate_bps;
87
+ }
88
+ }
@@ -0,0 +1,45 @@
1
+ /** @beta */
2
+ export declare const SHIPPING_MODE_AT_CART: readonly ["one_time", "subscription", "any"];
3
+ /** @beta */
4
+ export type ShippingModeAtCart = (typeof SHIPPING_MODE_AT_CART)[number];
5
+ /** @beta */
6
+ export declare const SHIPPING_VAT_RATE_KINDS: readonly ["fixed", "inherit_main_goods", "highest_in_cart"];
7
+ /** @beta */
8
+ export type ShippingVatRateKind = (typeof SHIPPING_VAT_RATE_KINDS)[number];
9
+ /** @beta */
10
+ export interface ShippingRule {
11
+ id: string;
12
+ name: string;
13
+ region_code: string;
14
+ currency: string;
15
+ mode_at_cart: ShippingModeAtCart;
16
+ carrier_kind: string;
17
+ free_threshold_minor: number | null;
18
+ fixed_cost_minor: number | null;
19
+ weight_threshold_g: number | null;
20
+ delivery_estimate_days: number | null;
21
+ priority: number;
22
+ vat_rate_bps: number;
23
+ vat_rate_kind: ShippingVatRateKind;
24
+ active: boolean;
25
+ valid_from: string;
26
+ valid_to: string | null;
27
+ }
28
+ /** @beta */
29
+ export interface ShippingResolverCart {
30
+ region_code: string;
31
+ currency: string;
32
+ cart_mode: "one_time" | "subscription";
33
+ subtotal_minor: number;
34
+ line_vat_rates_bps: ReadonlyArray<number>;
35
+ weight_total_g?: number;
36
+ }
37
+ /** @beta */
38
+ export interface ResolvedShipping {
39
+ amount_minor: number;
40
+ carrier_kind: string;
41
+ delivery_estimate_days: number | null;
42
+ applied_rule_id: string;
43
+ applied_rule_name: string;
44
+ vat_rate_bps: number;
45
+ }
@@ -0,0 +1,4 @@
1
+ /** @beta */
2
+ export const SHIPPING_MODE_AT_CART = ["one_time", "subscription", "any"];
3
+ /** @beta */
4
+ export const SHIPPING_VAT_RATE_KINDS = ["fixed", "inherit_main_goods", "highest_in_cart"];
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Subscription cycle hardening helpers — the canonical retry ladder a renewal
3
+ * payment failure follows.
4
+ *
5
+ * This module is the SINGLE authority on the ladder. It answers four questions:
6
+ *
7
+ * - `nextRetryAttemptAt(failedAt, attemptNumber, cadence?, failureClass?)` —
8
+ * when the next charge is due, or `null` when no next charge is due.
9
+ * - `shouldScheduleRetry(cycleStatus, attemptNumber, cadence?, failureClass?)`
10
+ * — whether a failed payment may move into `retry_scheduled`.
11
+ * - `ladderTerminatedByClass(failureClass, cadence?)` — whether the REASON for
12
+ * the refusal, rather than the rung reached, ends the ladder.
13
+ * - `isDunningLadderExhausted(cycle, cadence?)` — whether a refused cycle has
14
+ * run past the last rung, the only non-payment reason to pause.
15
+ *
16
+ * TERMINATING, not capping. An attempt past the end of the ladder answers
17
+ * `null`, which is the pause signal; it does not answer "the last slot again".
18
+ * Callers therefore never need a `attempt <= maxRetryAttempts()` guard of their
19
+ * own — asking for the next slot IS the decision, and a second copy of the
20
+ * budget check is exactly how two rails drift apart.
21
+ *
22
+ * The cadence is injectable so a deployment can publish its own ladder, and the
23
+ * default is frozen so nobody can mutate the shipped one in place.
24
+ *
25
+ * All helpers are PURE — they never schedule local jobs, never touch payment
26
+ * adapters, never persist a cycle.
27
+ */
28
+ /** @beta */
29
+ export interface CycleRetryCadence {
30
+ /**
31
+ * Hours after the failure at which each successive retry is due, in order.
32
+ * Its length IS the retry budget: `maxRetryAttempts()` reports it and
33
+ * `nextRetryAttemptAt` terminates past it.
34
+ */
35
+ readonly backoffHours: readonly number[];
36
+ /**
37
+ * Refusal classes for which this deployment publishes no ladder at all, named
38
+ * as {@link PaymentFailureClass} values. Typed as strings because the same set
39
+ * is read back out of a nullable `text` column written by an older deployment,
40
+ * and an unrecognised value must be ignored rather than crash a schedule.
41
+ *
42
+ * Omitting it means "no class ends the ladder early", which is the behaviour
43
+ * every deployment had before this field existed.
44
+ */
45
+ readonly terminatingFailureClasses?: readonly string[];
46
+ }
47
+ /** @beta */
48
+ export declare const DEFAULT_CYCLE_RETRY_CADENCE: CycleRetryCadence;
49
+ /** @beta */
50
+ export type CycleHandlerStatus = "planned" | "payment_pending" | "paid" | "payment_failed" | "retry_scheduled" | "skipped" | "cancelled";
51
+ /**
52
+ * Whether the class of a refusal, rather than the rung it reached, ends the
53
+ * ladder.
54
+ *
55
+ * Fails OPEN in three separate ways, because a wrong `true` here withholds a
56
+ * charge the customer expected while a wrong `false` merely retries as before:
57
+ * an absent class does not terminate, a class the cadence does not list does not
58
+ * terminate, and a listed class the taxonomy does not recognise does not
59
+ * terminate. The last check is why a typo in configuration cannot stop a ladder.
60
+ *
61
+ * The final word belongs to {@link failureClassDecision}, not to the list: a
62
+ * cadence that lists a class the decision table still permits retrying is
63
+ * incoherent, and the table wins. That keeps the schedule from ever contradicting
64
+ * the kernel that already ruled on whether the instrument may be charged again.
65
+ *
66
+ * @beta
67
+ */
68
+ export declare function ladderTerminatedByClass(failureClass: string | null | undefined, cadence?: CycleRetryCadence): boolean;
69
+ /** @beta */
70
+ export declare function nextRetryAttemptAt(failedAtIso: string, attemptNumber: number, cadence?: CycleRetryCadence, failureClass?: string | null): string | null;
71
+ /** @beta */
72
+ export declare function shouldScheduleRetry(cycleStatus: CycleHandlerStatus, attemptNumber: number, cadence?: CycleRetryCadence, failureClass?: string | null): boolean;
73
+ /** @beta */
74
+ export declare function maxRetryAttempts(cadence?: CycleRetryCadence): number;
75
+ /**
76
+ * Whether a refused cycle has EXHAUSTED the ladder: it failed, nothing is
77
+ * scheduled after it, and the attempt that failed sits past the last rung.
78
+ *
79
+ * This is the one condition under which the platform may pause a live
80
+ * subscription for non-payment. Both halves are required. An absent schedule on
81
+ * its own is not exhaustion: a class-terminated refusal on rung one also leaves
82
+ * nothing scheduled, and treating it as exhaustion would pause a subscription on
83
+ * the day of its first refusal. The rung is derived from the cadence, so pass
84
+ * the same cadence here and to `recordPaymentFailure`. The managed SQL dunning
85
+ * boundary spells the shipped ladder's rung as a fixed constant
86
+ * (`v_ladder_exhausted_from`, 4); a deployment that publishes a different
87
+ * ladder must move that constant with it.
88
+ *
89
+ * @beta
90
+ */
91
+ export declare function isDunningLadderExhausted(cycle: {
92
+ readonly status: CycleHandlerStatus;
93
+ readonly retryAttempt: number;
94
+ readonly nextRetryAt: string | null;
95
+ }, cadence?: CycleRetryCadence): boolean;