@owlmeans/payment 0.1.18-rc.36 → 0.1.18-rc.38

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 (166) hide show
  1. package/README.md +2 -2
  2. package/agent-meta/manifest.json +2 -2
  3. package/agent-meta/skills/entitlements/SKILL.md +4 -0
  4. package/agent-meta/skills/payment/SKILL.md +217 -13
  5. package/build/advertise.js +5 -3
  6. package/build/advertise.js.map +1 -1
  7. package/build/checkout/index.d.ts +3 -0
  8. package/build/checkout/index.d.ts.map +1 -0
  9. package/build/checkout/index.js +3 -0
  10. package/build/checkout/index.js.map +1 -0
  11. package/build/checkout/narrow.d.ts +25 -0
  12. package/build/checkout/narrow.d.ts.map +1 -0
  13. package/build/checkout/narrow.js +58 -0
  14. package/build/checkout/narrow.js.map +1 -0
  15. package/build/checkout/protocols.d.ts +32 -0
  16. package/build/checkout/protocols.d.ts.map +1 -0
  17. package/build/checkout/protocols.js +28 -0
  18. package/build/checkout/protocols.js.map +1 -0
  19. package/build/consts.d.ts +87 -0
  20. package/build/consts.d.ts.map +1 -1
  21. package/build/consts.js +127 -0
  22. package/build/consts.js.map +1 -1
  23. package/build/consumer/cancel.d.ts +11 -0
  24. package/build/consumer/cancel.d.ts.map +1 -0
  25. package/build/consumer/cancel.js +33 -0
  26. package/build/consumer/cancel.js.map +1 -0
  27. package/build/consumer/copy.d.ts +58 -0
  28. package/build/consumer/copy.d.ts.map +1 -0
  29. package/build/consumer/copy.js +91 -0
  30. package/build/consumer/copy.js.map +1 -0
  31. package/build/consumer/deadline.d.ts +43 -0
  32. package/build/consumer/deadline.d.ts.map +1 -0
  33. package/build/consumer/deadline.js +55 -0
  34. package/build/consumer/deadline.js.map +1 -0
  35. package/build/consumer/fifo.d.ts +40 -0
  36. package/build/consumer/fifo.d.ts.map +1 -0
  37. package/build/consumer/fifo.js +40 -0
  38. package/build/consumer/fifo.js.map +1 -0
  39. package/build/consumer/index.d.ts +10 -0
  40. package/build/consumer/index.d.ts.map +1 -0
  41. package/build/consumer/index.js +10 -0
  42. package/build/consumer/index.js.map +1 -0
  43. package/build/consumer/policy.d.ts +39 -0
  44. package/build/consumer/policy.d.ts.map +1 -0
  45. package/build/consumer/policy.js +122 -0
  46. package/build/consumer/policy.js.map +1 -0
  47. package/build/consumer/protocols.d.ts +105 -0
  48. package/build/consumer/protocols.d.ts.map +1 -0
  49. package/build/consumer/protocols.js +73 -0
  50. package/build/consumer/protocols.js.map +1 -0
  51. package/build/consumer/refund.d.ts +74 -0
  52. package/build/consumer/refund.d.ts.map +1 -0
  53. package/build/consumer/refund.js +123 -0
  54. package/build/consumer/refund.js.map +1 -0
  55. package/build/consumer/refusal.d.ts +10 -0
  56. package/build/consumer/refusal.d.ts.map +1 -0
  57. package/build/consumer/refusal.js +40 -0
  58. package/build/consumer/refusal.js.map +1 -0
  59. package/build/consumer/revive.d.ts +13 -0
  60. package/build/consumer/revive.d.ts.map +1 -0
  61. package/build/consumer/revive.js +26 -0
  62. package/build/consumer/revive.js.map +1 -0
  63. package/build/errors.d.ts +129 -0
  64. package/build/errors.d.ts.map +1 -1
  65. package/build/errors.js +221 -0
  66. package/build/errors.js.map +1 -1
  67. package/build/i18n/consumer-rights/be.json +123 -0
  68. package/build/i18n/consumer-rights/de.json +123 -0
  69. package/build/i18n/consumer-rights/en.json +123 -0
  70. package/build/i18n/consumer-rights/es.json +123 -0
  71. package/build/i18n/consumer-rights/fr.json +123 -0
  72. package/build/i18n/consumer-rights/pl.json +123 -0
  73. package/build/i18n/consumer-rights/ru.json +123 -0
  74. package/build/i18n/consumer-rights/uk.json +123 -0
  75. package/build/i18n/errors/be.json +9 -1
  76. package/build/i18n/errors/de.json +9 -1
  77. package/build/i18n/errors/en.json +9 -1
  78. package/build/i18n/errors/es.json +9 -1
  79. package/build/i18n/errors/fr.json +9 -1
  80. package/build/i18n/errors/pl.json +9 -1
  81. package/build/i18n/errors/ru.json +9 -1
  82. package/build/i18n/errors/uk.json +9 -1
  83. package/build/i18n.js +23 -0
  84. package/build/i18n.js.map +1 -1
  85. package/build/index.d.ts +3 -0
  86. package/build/index.d.ts.map +1 -1
  87. package/build/index.js +3 -0
  88. package/build/index.js.map +1 -1
  89. package/build/model/checkout.d.ts.map +1 -1
  90. package/build/model/checkout.js +3 -0
  91. package/build/model/checkout.js.map +1 -1
  92. package/build/model/consumer.d.ts +32 -0
  93. package/build/model/consumer.d.ts.map +1 -0
  94. package/build/model/consumer.js +425 -0
  95. package/build/model/consumer.js.map +1 -0
  96. package/build/model/estimate.d.ts.map +1 -1
  97. package/build/model/estimate.js +4 -2
  98. package/build/model/estimate.js.map +1 -1
  99. package/build/model/index.d.ts +1 -0
  100. package/build/model/index.d.ts.map +1 -1
  101. package/build/model/index.js +1 -0
  102. package/build/model/index.js.map +1 -1
  103. package/build/model/plan.d.ts.map +1 -1
  104. package/build/model/plan.js +10 -0
  105. package/build/model/plan.js.map +1 -1
  106. package/build/regions.d.ts +49 -0
  107. package/build/regions.d.ts.map +1 -0
  108. package/build/regions.js +85 -0
  109. package/build/regions.js.map +1 -0
  110. package/build/service.d.ts.map +1 -1
  111. package/build/service.js +9 -1
  112. package/build/service.js.map +1 -1
  113. package/build/types.d.ts +327 -3
  114. package/build/types.d.ts.map +1 -1
  115. package/package.json +11 -11
  116. package/src/advertise.ts +7 -3
  117. package/src/checkout/index.ts +2 -0
  118. package/src/checkout/narrow.ts +72 -0
  119. package/src/checkout/protocols.ts +60 -0
  120. package/src/consts.ts +140 -0
  121. package/src/consumer/cancel.ts +41 -0
  122. package/src/consumer/copy.ts +126 -0
  123. package/src/consumer/deadline.ts +82 -0
  124. package/src/consumer/fifo.ts +71 -0
  125. package/src/consumer/index.ts +9 -0
  126. package/src/consumer/policy.ts +131 -0
  127. package/src/consumer/protocols.ts +191 -0
  128. package/src/consumer/refund.ts +191 -0
  129. package/src/consumer/refusal.ts +41 -0
  130. package/src/consumer/revive.ts +59 -0
  131. package/src/errors.ts +302 -0
  132. package/src/i18n/consumer-rights/be.json +123 -0
  133. package/src/i18n/consumer-rights/de.json +123 -0
  134. package/src/i18n/consumer-rights/en.json +123 -0
  135. package/src/i18n/consumer-rights/es.json +123 -0
  136. package/src/i18n/consumer-rights/fr.json +123 -0
  137. package/src/i18n/consumer-rights/pl.json +123 -0
  138. package/src/i18n/consumer-rights/ru.json +123 -0
  139. package/src/i18n/consumer-rights/uk.json +123 -0
  140. package/src/i18n/errors/be.json +9 -1
  141. package/src/i18n/errors/de.json +9 -1
  142. package/src/i18n/errors/en.json +9 -1
  143. package/src/i18n/errors/es.json +9 -1
  144. package/src/i18n/errors/fr.json +9 -1
  145. package/src/i18n/errors/pl.json +9 -1
  146. package/src/i18n/errors/ru.json +9 -1
  147. package/src/i18n/errors/uk.json +9 -1
  148. package/src/i18n.ts +25 -0
  149. package/src/index.ts +3 -0
  150. package/src/model/checkout.ts +3 -0
  151. package/src/model/consumer.ts +469 -0
  152. package/src/model/estimate.ts +4 -2
  153. package/src/model/index.ts +1 -0
  154. package/src/model/plan.ts +10 -0
  155. package/src/regions.ts +115 -0
  156. package/src/service.ts +14 -2
  157. package/src/types.ts +358 -4
  158. package/tests/checkout-narrow.spec.ts +75 -0
  159. package/tests/consumer-copy.spec.ts +193 -0
  160. package/tests/consumer-deadline.spec.ts +86 -0
  161. package/tests/consumer-fifo.spec.ts +61 -0
  162. package/tests/consumer-protocols.spec.ts +103 -0
  163. package/tests/consumer-refund.spec.ts +136 -0
  164. package/tests/consumer-regions.spec.ts +140 -0
  165. package/tests/consumer-wire.spec.ts +128 -0
  166. package/tests/errors.spec.ts +75 -3
package/README.md CHANGED
@@ -8,7 +8,7 @@ server integration (`@owlmeans/server-payment`) implements against these contrac
8
8
  ## Installation
9
9
 
10
10
  ```bash
11
- bun add @owlmeans/payment@^0.1.18-rc.36
11
+ bun add @owlmeans/payment@^0.1.18-rc.38
12
12
  ```
13
13
 
14
14
  ## Concepts
@@ -129,7 +129,7 @@ This package ships embedded agent skills under `agent-meta/`. After installing y
129
129
  your project's skill store (`.agents/skills/`):
130
130
 
131
131
  ```sh
132
- npx @owlmeans/agent-skills@^0.1.18-rc.35
132
+ npx @owlmeans/agent-skills@^0.1.18-rc.38
133
133
  ```
134
134
 
135
135
  The embedded files are version-matched to this package release. Do not edit them
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "schemaVersion": 2,
3
3
  "package": "@owlmeans/payment",
4
- "version": "0.1.18-rc.36",
5
- "generatedAt": "2026-09-22T21:57:18.826Z",
4
+ "version": "0.1.18-rc.38",
5
+ "generatedAt": "2026-09-23T19:57:40.693Z",
6
6
  "canonicalRepo": "https://github.com/owlmeans/common",
7
7
  "entries": [
8
8
  {
@@ -141,6 +141,10 @@ reads the view through the pure selectors rather than polling it a second time.
141
141
  and rebuild them on unmarshal. Catch the class after `ResilientError.ensure`.
142
142
  - A UI phrases a refusal from `errors.<type>` and its fields (the reset date of an exhausted
143
143
  window), never from the message text.
144
+ - Not entitlement refusals: the consumer-rights refusals (`PerformanceConsentRequired`,
145
+ `SubscriptionStartRequired` 428; `BillingCountryLocked`, `WithdrawalUnavailable`,
146
+ `CancellationUnavailable` 409) and `CheckoutLimitExceeded` (409) declare their status and never
147
+ extend `AuthForbidden` — the `payment` skill, § Consumer rights.
144
148
 
145
149
  ## Related
146
150
 
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: payment
3
- description: "How to use @owlmeans/payment — provider-agnostic payment contracts, immutable payment protocols, amount/quantity checkout policies and pricing, catalogue records, and entitlement gates. Auto-invoked when importing payment types or errors, declaring paid routes, or creating checkout."
3
+ description: "How to use @owlmeans/payment — provider-agnostic payment contracts, immutable payment protocols, amount/quantity checkout policies, per-entity checkout narrowing and pricing, catalogue records, entitlement gates, and the EU consumer-rights contracts (billing regions, the policy record, withdrawal/cancellation views, refund calculators, 428/409 refusals, the legal copy, protocol factories). Auto-invoked when importing payment types or errors, declaring paid routes, creating checkout, or touching consent, withdrawal or cancellation."
4
4
  user-invocable: false
5
5
  ---
6
6
  <!-- AUTO-GENERATED — do not edit. Regenerate via sync-agent-meta. -->
@@ -8,11 +8,13 @@ user-invocable: false
8
8
  # @owlmeans/payment
9
9
 
10
10
  **Layer:** Core
11
- **Install:** `"@owlmeans/payment": "^0.1.18-rc.36"` in `dependencies`
11
+ **Install:** `"@owlmeans/payment": "^0.1.18-rc.38"` in `dependencies`
12
12
 
13
13
  The contracts half of payments: the catalogue (products, plans, localizations), amount- and
14
- quantity-priced checkout, and the entitlement model — plan capabilities, counted limits, promos,
15
- and the entitlement view the server gate and the browser both read. It talks to no paygate: a
14
+ quantity-priced checkout, the entitlement model — plan capabilities, counted limits, promos,
15
+ and the entitlement view the server gate and the browser both read — and the EU consumer-rights
16
+ contracts (billing regions, the policy record, withdrawal and cancellation views, the refund
17
+ calculators, the refusals, the legal copy, the protocol factories). It talks to no paygate: a
16
18
  server-side integration (`@owlmeans/server-payment`) implements against these, and
17
19
  `@owlmeans/client-payment` adapts the catalogue service for a browser. The model as a whole —
18
20
  admission, the usage ledger, the two gate services — is the `entitlements` skill.
@@ -22,8 +24,8 @@ admission, the usage ledger, the two gate services — is the `entitlements` ski
22
24
  | Export | Description |
23
25
  |---|---|
24
26
  | `makePaymentService(alias?)` · `appendPaymentService(ctx, alias?)` | The catalogue reader, registered under `DEFAULT_ALIAS` / `PAYMENT_SERVICE` (`'payment'`). |
25
- | `PaymentService` | `product(sku)` · `products()` · `plans(productSku, duration)` · `plan(planSku)` · `allPlans(productSku)` · `localize(lng, entity)` · `shallowAuthentication(token)`. |
26
- | `Product` · `ProductPlan` · `Localization` (+ schemas) | Catalogue records. `ProductPlan` carries `rank`, `free`, `gateways`, `suspendedAt`, `capabilities: PlanCapability[]`, `limits: { [key]: LimitDeclaration }`. |
27
+ | `PaymentService` | `product(sku)` · `products()` · `plans(productSku, duration)` · `plan(planSku)` · `allPlans(productSku)` · `localize(lng, entity)` · `shallowAuthentication(token)` · `pricingPolicy()` · `consumerRightsPolicy()` (`null` when undeclared). |
28
+ | `Product` · `ProductPlan` · `Localization` (+ schemas) | Catalogue records. `ProductPlan` carries `rank`, `free`, `gateways`, `suspendedAt`, `capabilities: PlanCapability[]`, `limits: { [key]: LimitDeclaration }`, `withdrawal.components` (§ Consumer rights). |
27
29
  | `PlanCapability` · `LimitDeclaration` · `PromoDeclaration` (+ schemas) | What a plan grants: a permission set, a counted allowance, a time box on either. |
28
30
  | `SubscriptionStatus` · `ENTITLING_STATUSES` · `TERMINAL_STATUSES` · `INTERNAL_PAYGATE` | Lifecycle vocabulary. `Active`/`Trial`/`PastDue` entitle; `Canceled`/`Expired`/`Ended`/`Blocked` are terminal; `Suspended` is revoked until resumed. |
29
31
  | `LimitKind` · `LimitWindow` · `PortalFlow` (+ schemas) | `window`/`lifetime`/`occupancy`; `day`/`month`; `manage`/`cancel`/`update`/`change`/`payment-method`. |
@@ -33,13 +35,22 @@ admission, the usage ledger, the two gate services — is the `entitlements` ski
33
35
  | `promoActive` · `promoViewOf` | Whether a promo is in force for one subscription at one instant. |
34
36
  | `EntitlementView` · `EntitlementPlanView` · `CapabilityView` · `LimitView` · `PromoView` · `LimitUsage` (+ wire schemas) | The entitlement view and its rows. |
35
37
  | `capabilityViewsOf` · `limitViewsOf` · `entitlementViewOf` · `reviveEntitlementView` · `capabilityOf` · `limitOf` · `hasLimitRoom` | Pure view builders and readers. |
36
- | `CreateCheckoutBody` (`planSku`) · `CreateCheckoutResponse` · `PortalLinkBody` · `PortalLinkResponse` (+ schemas) | Wire shapes an application's own checkout and portal protocols use. |
38
+ | `CreateCheckoutBody` (`planSku`, `country`, `startRequestId`) · `CreateCheckoutResponse` · `PortalLinkBody` · `PortalLinkResponse` (+ schemas) | Wire shapes an application's own checkout and portal protocols use. |
37
39
  | `CheckoutPricingMode` · `AmountCheckoutPolicy` / `QuantityCheckoutPolicy` (+ schemas) · `assertAmountCheckoutPolicy` · `assertQuantityCheckoutPolicy` · `assertCheckoutAmount` · `chargeAmountMinor` | Checkout pricing policies and their validation. |
38
40
  | `PricingPolicy` (+ schema) · `DEFAULT_PRICING_POLICY` · `assertPricingPolicy` · `TaxBehavior` · `TaxEstimateStatus` · `TaxType` (+ schemas) | Declared tax/currency behaviour (see § Pricing policy and tax estimate). |
39
41
  | `PriceEstimate` · `PriceEstimateBody` · `TaxEstimate` · `TaxRateEstimate` (+ schemas) · `estimateOf` · `ratePpmOf` | The tax/local-currency estimate read model and its pure math. |
40
42
  | `COUNTRY_CURRENCIES` · `currencyOfCountry` · `COUNTRY_CODES` · `CountrySchema` | ISO 3166-1 → ISO 4217 reference map for a country picker. |
41
- | `EntitlementRefusal` · `CapabilityRequired` · `LimitExhausted` | Refusals — all `AuthForbidden`. |
42
- | `PaymentError` · `PaygateError` · `UnknownPaygate` · `PaygateMappingError` · `WebhookSetupError` · `PortalUnavailable` · `ProductError` · `UnknownProduct` · `UnknownPlan` · `PlanRequired` · `PlanRankConflict` · `LimitUnknown` · `LimitMisdeclared` · `PaymentIdentificationError` · `SubscriptionError` · `UnknownSubscription` | Faults (500), except `PortalUnavailable` (409). Importing the package registers every type's message under `errors.<type>` in the seven languages. |
43
+ | `EntitlementRefusal` · `CapabilityRequired` · `LimitExhausted` | Entitlement refusals — all `AuthForbidden` (403). |
44
+ | `PaymentError` · `PaygateError` · `UnknownPaygate` · `PaygateMappingError` · `WebhookSetupError` · `PortalUnavailable` · `ProductError` · `UnknownProduct` · `UnknownPlan` · `PlanRequired` · `PlanRankConflict` · `LimitUnknown` · `LimitMisdeclared` · `PaymentIdentificationError` · `SubscriptionError` · `UnknownSubscription` · `ConsumerRightsError` | Faults (500), except `PortalUnavailable` (409). Importing the package registers every type's message under `errors.<type>` in eight languages (the seven of `SUPPORTED_LNGS` plus `fr`). |
45
+ | `ConsumerRightsRefusal` · `PerformanceConsentRequired` · `SubscriptionStartRequired` (428) · `BillingCountryLocked` · `WithdrawalUnavailable` · `CancellationUnavailable` · `CheckoutLimitExceeded` (409) · `consentRefusalOf` | Consumer-rights and checkout-limit refusals — declared statuses, never `AuthForbidden` (§ Consumer rights). |
46
+ | `ConsumerRegion` · `PurchaseKind` · `ConsentKind` · `DeclarationKind` · `DeclarationChannel` · `CancellationKind` · `WithdrawalStatus` · `CancellationStatus` · `WithdrawalUnavailableReason` · `CancellationUnavailableReason` (+ schemas) | Consumer-rights vocabulary. |
47
+ | `EU_COUNTRIES` · `EU_CONSUMER_TERRITORIES` · `EEA_EXTRA` · `CONSUMER_RIGHTS_TERRITORIES` · `COUNTRY_LANGUAGES` · `isEuCountry` · `isEeaCountry` · `regionOf` · `inScope` · `chargeCurrencyOf` · `billingLanguageOf` | Territories, region, scope, charge currency, legal language. |
48
+ | `ConsumerRightsPolicy` (+ `Links`, `Mechanisms`, schemas) · `DEFAULT_CONSUMER_RIGHTS` · `makeConsumerRightsPolicy` · `assertConsumerRightsPolicy` · `linksOf` · `CONSUMER_RIGHTS_RECORD_TYPE`/`_ID` | The consumer-rights policy record. |
49
+ | `BillingProfileView` · `PurchaseView`/`List` · `PerformanceConsentView`/`Body`/`Response` · `SubscriptionStartView`/`Query`/`Body`/`Response` · `WithdrawalEstimate` · `WithdrawalCandidate`/`List` · `WithdrawalBody` · `DeclarationReceipt` · `WithdrawalReceipt` · `CancellationBody` · `CancellationReceipt` · `ConsumerRightsPublicView` (+ schemas) · `revive*` | Consumer-rights wire shapes (ISO dates) and their revivers. |
50
+ | `withdrawalDeadlineOf` · `lastWithdrawalDayOf` · `withdrawalOpen` · `oneTimeWithdrawalRefund` · `subscriptionWithdrawalRefund` · `splitByShares` · `allocateFifo` · `unitsUsedAfter` · `cancellationEffectiveAt` | Pure calculators. |
51
+ | `CONSUMER_RIGHTS_RESOURCE` · `CONSUMER_RIGHTS_COPY_VERSION` · `consumerRightsCopy` · `consumerText` · `consentStatementOf` · `legalLabelsOf` · `placeholdersOf` | The legal copy. |
52
+ | `makeConsumerRightsProtocols` · `makeCheckoutReadProtocols` | Protocol factories. |
53
+ | `AmountNarrowing` · `narrowAmountPolicy` · `amountAllowed` · `CheckoutLimitView` · `AmountPolicyView`/`Query` · `PlanPriceView`/`List`/`PlanPricesQuery` (+ schemas) | Per-entity narrowing of an amount checkout, and synced plan prices. |
43
54
 
44
55
  Subpath: `./utils` — the `Config` / `Context` aliases to type your own context against.
45
56
 
@@ -172,6 +183,8 @@ A **limit** is counted: a `LimitDeclaration` under a key in `plan.limits`, asked
172
183
  - **Only `type` and `message` survive a marshal.** Both refusals pack their fields into the
173
184
  message and rebuild them in `finalizeUnmarshal()`; catch the class after
174
185
  `ResilientError.ensure`, never parse the text yourself.
186
+ - **Consumer-rights and checkout-limit refusals are not entitlement refusals**: they declare
187
+ 428/409 and never extend `AuthForbidden` (§ Consumer rights).
175
188
  - **Faults are not refusals**: `LimitUnknown` (a key no plan declares), `LimitMisdeclared`,
176
189
  `PlanRequired` (no plan resolvable, not even a free one), `PlanRankConflict`,
177
190
  `WebhookSetupError` are `PaymentError`s — a configuration or setup problem, not the user's plan
@@ -188,9 +201,12 @@ A **limit** is counted: a `LimitDeclaration` under a key in `plan.limits`, asked
188
201
  `CreateCheckoutBody` is the wire body of a checkout: `productSku`, `planSku` for a subscription,
189
202
  `entitySlug` (the only organization value on a public body — a server resolves the stable
190
203
  `entityId` before persisting anything), `service`, `amountMinor` for an amount checkout, return
191
- URLs. The package declares no protocols: an application declares its own checkout and portal
192
- protocols over `CreateCheckoutBodySchema` / `PortalLinkBodySchema`; `PortalLinkBody.flow` picks the portal flow and
193
- `planSku` names the target of a `change`.
204
+ URLs, `country` (the declared billing country; a locked profile overrides it) and
205
+ `startRequestId` (the subscription start request recorded just before). The package declares no
206
+ fixed checkout protocols: an application declares its own checkout and portal protocols over
207
+ `CreateCheckoutBodySchema` / `PortalLinkBodySchema`; `PortalLinkBody.flow` picks the portal flow
208
+ and `planSku` names the target of a `change`. The read side and the consumer-rights surface come
209
+ as factories (§ Protocol factories).
194
210
 
195
211
  ### Amount checkout
196
212
 
@@ -209,6 +225,29 @@ to `amountMinor`, not the adjusted checkout subtotal or tax-inclusive total.
209
225
  Quantity checkout remains supported. It uses its reusable unit price and quantity policy; do not
210
226
  infer a pricing mode from the presence of `amountMinor`.
211
227
 
228
+ ### Narrowing an amount checkout per entity
229
+
230
+ The plan's `amountPolicy` is the same for everyone; what one entity may buy NOW is narrower when a
231
+ checkout plugin (a spending tier, a rolling cap, a fraud hold) answers an `AmountNarrowing`
232
+ `{ maximumMinor, reason, resetsAt?, remainingMinor? }`. `narrowAmountPolicy(base, narrowings,
233
+ { productSku?, planSku? })` is the one computation the server's refusal and the dialog's control
234
+ share, so they cannot disagree:
235
+
236
+ - the maximum is the smallest of the base maximum and every narrowing's (negative or fractional
237
+ values floor at 0); `reason`/`resetsAt`/`remainingMinor` come from the narrowing that set it,
238
+ the first on a tie;
239
+ - presets above the maximum are dropped, the default is clamped into `[minimum, maximum]`;
240
+ - `limit` is `null` when nothing lowered the base maximum — no note to show;
241
+ - **`limit.blocked`** (`limit.maximumMinor < base.minimumMinor`) means no amount may be bought now.
242
+ The returned `policy` is then still a VALID policy (`assertAmountCheckoutPolicy` accepts it),
243
+ pinned to the minimum with no presets, so a validator never throws — but it is NOT an offer of
244
+ the minimum: a UI disables the input and the confirm button on `blocked`, and a server refuses
245
+ every amount with `CheckoutLimitExceeded`. `amountAllowed(view, amount)` answers exactly that.
246
+
247
+ `CheckoutLimitExceeded` (409) packs `checkout-limit-exceeded:<encodeURIComponent(reason)>:
248
+ <maximumMinor>:<currency>[:<resetsAt ISO>]` and rebuilds `reason`, `maximumMinor`, `currency`,
249
+ `resetsAt`. It is a payment refusal, not an entitlement one.
250
+
212
251
  ## Pricing policy and tax estimate
213
252
 
214
253
  `PricingPolicy` (a `PRICING_POLICY_RECORD_ID` singleton config record, `declarePaymentPricing` in
@@ -223,7 +262,9 @@ or `DEFAULT_PRICING_POLICY` — the fixed behaviour every checkout had before th
223
262
  an application that declares nothing sees no change.
224
263
 
225
264
  `PriceEstimate` (built by `@owlmeans/server-payment`'s gateway, `estimatePrice`) is the read model:
226
- `country`/`source` (`'request'`/`'customer'`), `currency`, `behavior`, `tax: TaxEstimate` (`status`
265
+ `country`/`source` (`'request'`/`'customer'`, or `'profile'` with `locked: true` when the entity's
266
+ billing country is locked and overrides the request — a picker then shows it and does not change
267
+ it), `region`, `currency`, `behavior`, `tax: TaxEstimate` (`status`
227
268
  one of `TaxEstimateStatus` — `taxed`/`reverse-charge`/`none`/`at-checkout`/`location-required` —
228
269
  plus `subtotalMinor`/`taxMinor`/`totalMinor`, `scalable`, and `rates: TaxRateEstimate[]` with
229
270
  `ratePpm` parsed by `ratePpmOf` from Stripe's `percentage_decimal`, never `Number(x) * 10_000`),
@@ -238,6 +279,169 @@ side by side. `COUNTRY_CURRENCIES` / `currencyOfCountry` / `COUNTRY_CODES` is a
238
279
  ISO 4217 map for a country picker and the local-currency lookup — not a Stripe list, and a country
239
280
  absent from it still gets a tax estimate, just no local-currency line.
240
281
 
282
+ ## Consumer rights (EU withdrawal, cancellation, country lock)
283
+
284
+ The contracts behind the right of withdrawal (CRD Art. 9–16, the Art. 11a withdrawal function),
285
+ the cancellation function (§ 312k BGB / L215-1-1) and a billing country fixed at the first
286
+ purchase. `@owlmeans/server-payment` records, enforces and mails; `@owlmeans/web-payment` renders.
287
+ Nothing here is product copy — the trader, plan and product names are placeholders.
288
+
289
+ ### Territories, region, scope
290
+
291
+ - `EU_COUNTRIES` (27, Greece is `GR`), `EU_CONSUMER_TERRITORIES` (plus AX, GF, GP, MQ, RE, YT, MF —
292
+ consumer law applies there even where EU VAT does not), `EEA_EXTRA` (IS, LI, NO) and their union
293
+ `CONSUMER_RIGHTS_TERRITORIES`, the default `policy.countries`.
294
+ - `regionOf(country, policy?)`: `Eu` inside the policy's territories, `Other` outside, `null` for no
295
+ country. `inScope(region, country, policy)`: a known country decides by the territories, else a
296
+ known region, else `unknownCountry` — `'protect'` by default, so an unknown buyer is protected.
297
+ - `chargeCurrencyOf(region, policy, fallback)`: the policy's currency for the region; an unknown
298
+ region reads as `Eu`. The display currency is the charge currency, everywhere.
299
+ - `billingLanguageOf(country, policy?, fallback?)`: `policy.languages`, then `COUNTRY_LANGUAGES`
300
+ (unambiguous countries only — BE, LU, CH are absent), then `fallback`, then
301
+ `policy.defaultLanguage`. Legal copy is shown in this language, with a toggle to the UI language.
302
+
303
+ ### The policy record
304
+
305
+ `ConsumerRightsPolicy` is a singleton config record (`CONSUMER_RIGHTS_RECORD_ID`), declared by the
306
+ server integration through `makeConsumerRightsPolicy(def)` — `DEFAULT_CONSUMER_RIGHTS` filled in
307
+ (14 days, weekend rollover, margin 5 — a period ending on a public holiday runs to the next working
308
+ day and holiday clusters need up to five, no per-country calendar is kept —, every mechanism OFF,
309
+ `defaultLanguage: 'en'`, start requests
310
+ usable 3600 s) — and `assertConsumerRightsPolicy` (non-empty `textVersion`, `^[A-Z]{2}$`
311
+ countries, `withdrawalDays >= 14`, margin 0..7, lowercase currencies, https links, the default
312
+ language present, `withdrawalInformation` required while `withdrawal` or `performanceConsent` is
313
+ on; throws `ConsumerRightsError('policy:<field>')`). It carries only public links, territories
314
+ and switches — mail options live in a backend-only plugin config — so it is ADVERTISED to the
315
+ browser like the pricing policy. `PaymentService.consumerRightsPolicy()` answers `null` when none
316
+ is declared: no consumer-rights behaviour at all. `linksOf(policy, lng)` merges a language's links
317
+ field by field over the default language's (`de-AT` reads `de`).
318
+
319
+ `ProductPlan.withdrawal.components: PlanWithdrawalComponent[]` (`{ key, basis: 'time' | 'units',
320
+ shareMinor }`) states the separately priced parts of a subscription (CJEU C-641/19 PE Digital:
321
+ without them the whole price is pro rata by time).
322
+
323
+ ### Wire views
324
+
325
+ Every view schema carries dates as ISO strings (as `model/view.ts` does) and is revived with its
326
+ `revive*` helper (`reviveConsentView`, `revivePurchase`/`List`, `reviveBillingProfile`,
327
+ `reviveConsentResponse`, `reviveStartResponse`, `reviveWithdrawalList`, `reviveReceipt`,
328
+ `reviveCheckoutLimit`, `reviveAmountPolicyView`) — idempotent. A nullable enum on the wire lists
329
+ `null` in its enum (ajv rejects `null` otherwise). The consent and start views carry `trader`, the
330
+ name the statement is rendered with, so the server's record and the dialog's text are identical.
331
+ `WithdrawalBody` and `CancellationBody` ask only for name, contract and e-mail (plus the
332
+ cancellation kind, reason and date) and accept an optional `honeypot` a person never fills. A
333
+ public declaration answers `DeclarationReceipt` — what was declared and when, never whether a
334
+ contract matched.
335
+
336
+ ### Calculators — always in the consumer's favour
337
+
338
+ All amounts in BigInt; a deduction rounds DOWN, a refund rounds UP, a refund never exceeds what is
339
+ still unrefunded.
340
+
341
+ - `withdrawalDeadlineOf(purchasedAt, rule | policy)` → the EXCLUSIVE end: the purchase's UTC day +
342
+ `days`, a Saturday/Sunday last day moved to Monday, + `marginDays`, start of the next UTC day.
343
+ Wed 2026-09-23 → 2026-10-13T00:00Z; Sat 2026-09-26 → 2026-10-18T00:00Z; Sun 2026-12-20 →
344
+ 2027-01-10T00:00Z (margin 5). `withdrawalOpen(window, at)` — before the deadline, not withdrawn,
345
+ not refunded. **A person is shown the last included day**, `lastWithdrawalDayOf(deadline)` (the
346
+ UTC day of `deadline − 1 ms`), phrased "until the end of <date>" — never the exclusive instant.
347
+ - `oneTimeWithdrawalRefund({ paidMinor, refundedMinor?, unitsGranted, unitsUsed })` →
348
+ `min(paid − refunded, ceil(paid × (granted − used) / granted))`; `unitsUsed` is the deduction —
349
+ units used AFTER consent (none before it) plus debt settled from the lot plus units already
350
+ clawed back (`@owlmeans/server-payment` passes the meter's `usedAfter + settled + clawed`);
351
+ returns `unitsReturned`. 1256 paid, 125k of 500k used → 942.
352
+ - `subscriptionWithdrawalRefund(...)` splits `netMinor` by the components (`splitByShares`, largest
353
+ remainder); a `time` part loses `floor(c × elapsedDays / periodDays)` counted from
354
+ `max(periodStart, servicesRequestedAt)` — NOTHING without a start request; a `units` part loses
355
+ `floor(c × used / granted)`; gross = `ceil(paid × refundNet / net)`. Net 2000 / paid 2460, 3 of 30
356
+ days and 100k of 500k used → 2091.
357
+ - `allocateFifo(lots, spends)` — a spend takes the oldest lot granted at or before it; overflow is
358
+ `unallocated`. `unitsUsedAfter(lot, consentedAt)` counts slices strictly after the consent, `0`
359
+ without one.
360
+ - `cancellationEffectiveAt(periodEnd, 'month' | 'year', requested?)` — the period end, or the first
361
+ boundary on or after a later requested date (end-of-month clamped from the anchor).
362
+
363
+ ### Refusals
364
+
365
+ `ConsumerRightsRefusal` extends `ConsumerRightsError` (a `PaymentError`), NEVER `AuthForbidden`:
366
+ an HTTP boundary tests that family first, and a 403 would hide the status a client acts on. Each
367
+ refusal declares `static httpStatus`, packs its fields into the message and rebuilds them in
368
+ `finalizeUnmarshal()` (the marker is read after its LAST occurrence, so a registry rebuild of a
369
+ doubled prefix still parses):
370
+
371
+ | Class | Status | Marker (after `payment:consumer-rights:`) | Fields |
372
+ |---|---|---|---|
373
+ | `PerformanceConsentRequired` | 428 | `performance-consent-required:<pending>[:<deadline ISO>]` | `pending`, `deadline` |
374
+ | `SubscriptionStartRequired` | 428 | `subscription-start-required:<encodeURIComponent(planSku)>` | `planSku` (pass it raw) |
375
+ | `BillingCountryLocked` | 409 | `billing-country-locked:<country>[:<requested>]` | `country`, `requested` |
376
+ | `WithdrawalUnavailable` | 409 | `withdrawal-unavailable:<WithdrawalUnavailableReason>` | `reason` |
377
+ | `CancellationUnavailable` | 409 | `cancellation-unavailable:<CancellationUnavailableReason>` | `reason` |
378
+
379
+ `consentRefusalOf(e)` → `'performance' | 'subscription-start' | null`: the class after
380
+ `ResilientError.ensure`, else the marker or type name in `message`/`type`. A production body
381
+ carries only an incident id — a bare 428 is the caller's to read with `@owlmeans/api/status`.
382
+
383
+ ### The legal copy
384
+
385
+ The i18n resource `payment-consumer-rights` (`CONSUMER_RIGHTS_RESOURCE`, library tier, `lib`
386
+ namespace), in en pl ru be uk es de fr, versioned by `CONSUMER_RIGHTS_COPY_VERSION` (bump it on ANY
387
+ change to a bundle). Branches: `performance-consent`, `subscription-start` (`title`, `intro`,
388
+ `request`, `acknowledgement`, `checkbox` = request + space + acknowledgement, `confirm`,
389
+ `decline`; the consent branch also `purchase`), `withdrawal`, `cancellation` (form labels and the
390
+ statutory buttons `function` / `confirm`), `links`, `checkout` (`terms-acceptance.{in-scope,
391
+ other}` markdown, `renewal.{month, year, after-submit}`, `price.{exclusive, inclusive, exclusive-tax}` (VAT wording for the territories, "applicable tax" for a buyer outside them), `top-up`,
392
+ `top-up-note`), `email.{common, consent, start, purchase, withdrawal, cancellation}` (the purchase
393
+ mail carries the CRD Annex I(A) withdrawal information with the function's address and the Annex
394
+ I(B) model form, per language from the national models; there `{{trader}}` is the trader's whole
395
+ identity — legal name, address, e-mail; a `review` withdrawal receipt promises the reimbursement
396
+ within the statutory 14 days; `email.cancellation.review` answers an extraordinary cancellation,
397
+ whose status is `CancellationStatus.Review`), `credit-note.memo`. In the consent and start
398
+ STATEMENTS `{{trader}}` is the trader's short name.
399
+
400
+ - Read it with `consumerRightsCopy(lng)` (any language, merged key by key over English, no i18next
401
+ instance, never drains a bundle — `resolveI18nResource`), `consumerText(lng, path, vars)` (fills
402
+ `{{name}}`; throws `ConsumerRightsError('copy:<path>[:<name>]')` on a missing text or value — a
403
+ legal text never goes out with a hole), `consentStatementOf(lng, kind, { trader, plan? })` (the
404
+ exact statement the dialog shows, the server records and the mail repeats) and
405
+ `legalLabelsOf(lng)`.
406
+ - Statutory labels are pinned by a test: en "Withdraw from contract here" / "Confirm withdrawal",
407
+ "Cancel contracts here" / "Cancel now"; de "Vertrag widerrufen" / "Widerruf bestätigen",
408
+ "Verträge hier kündigen" / "Jetzt kündigen"; fr "Renoncer au contrat ici" / "Confirmer la
409
+ rétractation", "Résilier votre contrat" / "Notification de la résiliation"; pl "Odstąp od umowy
410
+ tutaj" / "Potwierdź odstąpienie od umowy", "Wypowiedz umowę tutaj" / "Wypowiedz teraz"; es
411
+ "Desistir del contrato aquí" / "Confirmar el desistimiento", "Cancelar contratos aquí" /
412
+ "Cancelar ahora"; uk/ru/be are courtesy translations.
413
+ - The express request uses the statutory verbs (PL "Żądam i wyrażam wyraźną zgodę … Przyjmuję do
414
+ wiadomości …", DE "Ich verlange ausdrücklich und stimme ausdrücklich zu … Mir ist bekannt …", FR
415
+ "Je demande expressément et j’accepte expressément … Je reconnais perdre …").
416
+ - Wording rule, enforced by a scan of every language: say "the right of withdrawal expires" and
417
+ "only unused credits are reimbursed" — never non-refundable / nicht erstattungsfähig / non
418
+ remboursable / bezzwrotny / no reembolsable / невозвратный / неповоротний / незваротны.
419
+ - Paygate texts (`checkout.*`) stay within 1200 characters after interpolating long URLs.
420
+ - An application overrides a text with `addI18nApp(lng, CONSUMER_RIGHTS_RESOURCE, data, { ns:
421
+ LIB_NAMESPACE })`; every placeholder of a text must match its English master.
422
+
423
+ ### Protocol factories
424
+
425
+ `makeConsumerRightsProtocols({ prefix, parent | guards (+ gate), path = '/consumer-rights',
426
+ public?: false | { path = '/public/consumer-rights', parent?, screens?: { withdrawal?,
427
+ cancellation?, parent? } } })`, the `makeMarketingConsentProtocols` way:
428
+
429
+ - account subtree under the app's GUARDED parent (its guards and gate inherited): `base`,
430
+ `profile` GET `/profile`, `purchases` GET `/purchases`, `consent` GET / `giveConsent` POST
431
+ `/consent`, `start` GET `/start?planSku` / `requestStart` POST `/start`, `withdrawals` GET /
432
+ `withdraw` POST `/withdrawal`, `cancel` POST `/cancellation`; aliases `<prefix>:<name>`
433
+ (`consent:give`, `start:request`);
434
+ - `public` (only when declared): `base`, `policy` GET `/policy`, `withdraw` POST `/withdrawal`,
435
+ `cancel` POST `/cancellation` — NO guard and NO gate (like `paymentGate.webhook`; a public
436
+ `parent` must be unguarded), receipts typed `DeclarationReceipt`; `screens` declares BOTH sticky
437
+ frontend routes (`/legal/withdraw`, `/legal/cancel` by default); aliases `<prefix>:public:<name>`;
438
+ - neither `parent` nor `guards` is a `SyntaxError`; absent parts are left OUT of the tree (never
439
+ `undefined`), so `protocols(tree)` and `mapProtocols` walk it as is.
440
+
441
+ `makeCheckoutReadProtocols({ prefix, parent | guards })` → `base` (`/checkout`), `amountPolicy`
442
+ GET `/amount-policy?productSku&planSku` (`AmountPolicyView`) and `planPrices` GET
443
+ `/plan-prices?productSku` (`PlanPriceList`).
444
+
241
445
  ## `shallowAuthentication` identifies, it does not authorize
242
446
 
243
447
  It reads the `profileId` out of an envelope token WITHOUT verifying the signature, and throws
@@ -1,13 +1,15 @@
1
1
  import { CONFIG_RECORD } from '@owlmeans/context';
2
2
  import { apiConfigPlugin, every } from '@owlmeans/api-config';
3
- import { L10N_RECORD_TYPE, PLAN_RECORD_TYPE, PRICING_POLICY_RECORD_TYPE, PRODUCT_RECORD_TYPE } from './consts.js';
3
+ import { CONSUMER_RIGHTS_RECORD_TYPE, L10N_RECORD_TYPE, PLAN_RECORD_TYPE, PRICING_POLICY_RECORD_TYPE, PRODUCT_RECORD_TYPE, } from './consts.js';
4
4
  /**
5
5
  * The pricing policy record carries only flags and TTLs (never a Stripe secret, an API version, or
6
6
  * a migration switch — those stay in a backend-only plugin), so advertising it to the browser is
7
- * safe.
7
+ * safe. The consumer-rights policy record carries only public links, territories and switches
8
+ * (the mail options — sender, archive copy — stay in a backend-only plugin config), so the browser
9
+ * renders the same links and switches the server enforces.
8
10
  */
9
11
  const advertisedRecordTypes = new Set([
10
- L10N_RECORD_TYPE, PLAN_RECORD_TYPE, PRODUCT_RECORD_TYPE, PRICING_POLICY_RECORD_TYPE,
12
+ L10N_RECORD_TYPE, PLAN_RECORD_TYPE, PRODUCT_RECORD_TYPE, PRICING_POLICY_RECORD_TYPE, CONSUMER_RIGHTS_RECORD_TYPE,
11
13
  ]);
12
14
  apiConfigPlugin({
13
15
  allow: {
@@ -1 +1 @@
1
- {"version":3,"file":"advertise.js","sourceRoot":"","sources":["../src/advertise.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAA;AACjD,OAAO,EAAE,eAAe,EAAE,KAAK,EAAE,MAAM,sBAAsB,CAAA;AAC7D,OAAO,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,0BAA0B,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAA;AAEjH;;;;GAIG;AACH,MAAM,qBAAqB,GAAG,IAAI,GAAG,CAAC;IACpC,gBAAgB,EAAE,gBAAgB,EAAE,mBAAmB,EAAE,0BAA0B;CACpF,CAAC,CAAA;AAEF,eAAe,CAAC;IACd,KAAK,EAAE;QACL,CAAC,aAAa,CAAC,EAAE,KAAK,CACpB,IAAI,EACJ,KAAK,CAAC,EAAE,CAAC,KAAK,IAAI,IAAI,IAAI,OAAO,KAAK,KAAK,QAAQ;eAC9C,qBAAqB,CAAC,GAAG,CAAE,KAAiC,CAAC,UAAU,IAAI,EAAE,CAAC,CACpF;KACF;CACF,CAAC,CAAA"}
1
+ {"version":3,"file":"advertise.js","sourceRoot":"","sources":["../src/advertise.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAA;AACjD,OAAO,EAAE,eAAe,EAAE,KAAK,EAAE,MAAM,sBAAsB,CAAA;AAC7D,OAAO,EACL,2BAA2B,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,0BAA0B,EAAE,mBAAmB,GACjH,MAAM,aAAa,CAAA;AAEpB;;;;;;GAMG;AACH,MAAM,qBAAqB,GAAG,IAAI,GAAG,CAAC;IACpC,gBAAgB,EAAE,gBAAgB,EAAE,mBAAmB,EAAE,0BAA0B,EAAE,2BAA2B;CACjH,CAAC,CAAA;AAEF,eAAe,CAAC;IACd,KAAK,EAAE;QACL,CAAC,aAAa,CAAC,EAAE,KAAK,CACpB,IAAI,EACJ,KAAK,CAAC,EAAE,CAAC,KAAK,IAAI,IAAI,IAAI,OAAO,KAAK,KAAK,QAAQ;eAC9C,qBAAqB,CAAC,GAAG,CAAE,KAAiC,CAAC,UAAU,IAAI,EAAE,CAAC,CACpF;KACF;CACF,CAAC,CAAA"}
@@ -0,0 +1,3 @@
1
+ export * from './narrow.js';
2
+ export * from './protocols.js';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/checkout/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAA;AAC3B,cAAc,gBAAgB,CAAA"}
@@ -0,0 +1,3 @@
1
+ export * from './narrow.js';
2
+ export * from './protocols.js';
3
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/checkout/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAA;AAC3B,cAAc,gBAAgB,CAAA"}
@@ -0,0 +1,25 @@
1
+ import type { AmountCheckoutPolicy, AmountNarrowing, AmountPolicyView } from '../types.js';
2
+ export interface NarrowAmountPolicyOptions {
3
+ productSku?: string;
4
+ planSku?: string;
5
+ }
6
+ /**
7
+ * An amount policy narrowed for one entity — the one computation the server's refusal and the
8
+ * dialog's control share, so a disabled control and a refusal cannot disagree.
9
+ *
10
+ * - The maximum is the smallest of the base maximum and every narrowing's; the reason, reset and
11
+ * remaining come from the narrowing that set it (the first on a tie).
12
+ * - Presets above the maximum are dropped; the default is clamped into `[minimum, maximum]`.
13
+ * - `limit` is `null` when no narrowing lowered the base maximum.
14
+ * - `blocked` (`limit.maximumMinor < base.minimumMinor`) means NO amount may be bought now. The
15
+ * returned `policy` then stays a VALID policy pinned to the minimum (maximum = default =
16
+ * minimum, no presets above it), so a validator never throws on it; the UI disables the input
17
+ * and the confirm button on `limit.blocked`, and the server refuses every amount with
18
+ * `CheckoutLimitExceeded` — neither may read the pinned policy as "the minimum is allowed".
19
+ *
20
+ * @throws PaymentError (`checkout-policy:*`) when the base policy itself is invalid.
21
+ */
22
+ export declare const narrowAmountPolicy: (base: AmountCheckoutPolicy, narrowings: AmountNarrowing[], opts?: NarrowAmountPolicyOptions) => AmountPolicyView;
23
+ /** Whether an amount may be bought under a narrowed view: never when blocked. */
24
+ export declare const amountAllowed: (view: AmountPolicyView, amountMinor: number) => boolean;
25
+ //# sourceMappingURL=narrow.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"narrow.d.ts","sourceRoot":"","sources":["../../src/checkout/narrow.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,oBAAoB,EAAE,eAAe,EAAE,gBAAgB,EAAqB,MAAM,aAAa,CAAA;AAE7G,MAAM,WAAW,yBAAyB;IACxC,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB;AAKD;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,kBAAkB,SACvB,oBAAoB,cAAc,eAAe,EAAE,SAAQ,yBAAyB,KACzF,gBAqCF,CAAA;AAED,iFAAiF;AACjF,eAAO,MAAM,aAAa,SAAU,gBAAgB,eAAe,MAAM,KAAG,OAEW,CAAA"}
@@ -0,0 +1,58 @@
1
+ import { assertAmountCheckoutPolicy } from '../pricing.js';
2
+ const maximumOf = (narrowing) => Number.isFinite(narrowing.maximumMinor) ? Math.max(0, Math.floor(narrowing.maximumMinor)) : 0;
3
+ /**
4
+ * An amount policy narrowed for one entity — the one computation the server's refusal and the
5
+ * dialog's control share, so a disabled control and a refusal cannot disagree.
6
+ *
7
+ * - The maximum is the smallest of the base maximum and every narrowing's; the reason, reset and
8
+ * remaining come from the narrowing that set it (the first on a tie).
9
+ * - Presets above the maximum are dropped; the default is clamped into `[minimum, maximum]`.
10
+ * - `limit` is `null` when no narrowing lowered the base maximum.
11
+ * - `blocked` (`limit.maximumMinor < base.minimumMinor`) means NO amount may be bought now. The
12
+ * returned `policy` then stays a VALID policy pinned to the minimum (maximum = default =
13
+ * minimum, no presets above it), so a validator never throws on it; the UI disables the input
14
+ * and the confirm button on `limit.blocked`, and the server refuses every amount with
15
+ * `CheckoutLimitExceeded` — neither may read the pinned policy as "the minimum is allowed".
16
+ *
17
+ * @throws PaymentError (`checkout-policy:*`) when the base policy itself is invalid.
18
+ */
19
+ export const narrowAmountPolicy = (base, narrowings, opts = {}) => {
20
+ assertAmountCheckoutPolicy(base);
21
+ let setter = null;
22
+ let maximum = base.maximumMinor;
23
+ for (const narrowing of narrowings) {
24
+ const candidate = maximumOf(narrowing);
25
+ if (candidate < maximum) {
26
+ maximum = candidate;
27
+ setter = narrowing;
28
+ }
29
+ }
30
+ if (setter == null) {
31
+ return { policy: base, limit: null };
32
+ }
33
+ const blocked = maximum < base.minimumMinor;
34
+ const ceiling = blocked ? base.minimumMinor : maximum;
35
+ const policy = {
36
+ ...base,
37
+ maximumMinor: ceiling,
38
+ defaultMinor: Math.min(Math.max(base.defaultMinor, base.minimumMinor), ceiling),
39
+ presetsMinor: base.presetsMinor.filter(preset => preset <= ceiling),
40
+ };
41
+ const limit = {
42
+ productSku: opts.productSku ?? '',
43
+ ...(opts.planSku != null ? { planSku: opts.planSku } : {}),
44
+ currency: base.currency,
45
+ minimumMinor: base.minimumMinor,
46
+ maximumMinor: maximum,
47
+ narrowed: true,
48
+ blocked,
49
+ reason: setter.reason,
50
+ ...(setter.resetsAt != null ? { resetsAt: setter.resetsAt } : {}),
51
+ ...(setter.remainingMinor != null ? { remainingMinor: setter.remainingMinor } : {}),
52
+ };
53
+ return { policy: assertAmountCheckoutPolicy(policy), limit };
54
+ };
55
+ /** Whether an amount may be bought under a narrowed view: never when blocked. */
56
+ export const amountAllowed = (view, amountMinor) => view.limit?.blocked !== true && Number.isSafeInteger(amountMinor)
57
+ && amountMinor >= view.policy.minimumMinor && amountMinor <= view.policy.maximumMinor;
58
+ //# sourceMappingURL=narrow.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"narrow.js","sourceRoot":"","sources":["../../src/checkout/narrow.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,0BAA0B,EAAE,MAAM,eAAe,CAAA;AAQ1D,MAAM,SAAS,GAAG,CAAC,SAA0B,EAAU,EAAE,CACvD,MAAM,CAAC,QAAQ,CAAC,SAAS,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;AAE/F;;;;;;;;;;;;;;;GAeG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAChC,IAA0B,EAAE,UAA6B,EAAE,IAAI,GAA8B,EAAE,EAC7E,EAAE;IACpB,0BAA0B,CAAC,IAAI,CAAC,CAAA;IAChC,IAAI,MAAM,GAA2B,IAAI,CAAA;IACzC,IAAI,OAAO,GAAG,IAAI,CAAC,YAAY,CAAA;IAC/B,KAAK,MAAM,SAAS,IAAI,UAAU,EAAE,CAAC;QACnC,MAAM,SAAS,GAAG,SAAS,CAAC,SAAS,CAAC,CAAA;QACtC,IAAI,SAAS,GAAG,OAAO,EAAE,CAAC;YACxB,OAAO,GAAG,SAAS,CAAA;YACnB,MAAM,GAAG,SAAS,CAAA;QACpB,CAAC;IACH,CAAC;IACD,IAAI,MAAM,IAAI,IAAI,EAAE,CAAC;QACnB,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAA;IACtC,CAAC;IAED,MAAM,OAAO,GAAG,OAAO,GAAG,IAAI,CAAC,YAAY,CAAA;IAC3C,MAAM,OAAO,GAAG,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC,CAAC,OAAO,CAAA;IACrD,MAAM,MAAM,GAAyB;QACnC,GAAG,IAAI;QACP,YAAY,EAAE,OAAO;QACrB,YAAY,EAAE,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,YAAY,EAAE,IAAI,CAAC,YAAY,CAAC,EAAE,OAAO,CAAC;QAC/E,YAAY,EAAE,IAAI,CAAC,YAAY,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC,MAAM,IAAI,OAAO,CAAC;KACpE,CAAA;IACD,MAAM,KAAK,GAAsB;QAC/B,UAAU,EAAE,IAAI,CAAC,UAAU,IAAI,EAAE;QACjC,GAAG,CAAC,IAAI,CAAC,OAAO,IAAI,IAAI,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC1D,QAAQ,EAAE,IAAI,CAAC,QAAQ;QACvB,YAAY,EAAE,IAAI,CAAC,YAAY;QAC/B,YAAY,EAAE,OAAO;QACrB,QAAQ,EAAE,IAAI;QACd,OAAO;QACP,MAAM,EAAE,MAAM,CAAC,MAAM;QACrB,GAAG,CAAC,MAAM,CAAC,QAAQ,IAAI,IAAI,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,MAAM,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACjE,GAAG,CAAC,MAAM,CAAC,cAAc,IAAI,IAAI,CAAC,CAAC,CAAC,EAAE,cAAc,EAAE,MAAM,CAAC,cAAc,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KACpF,CAAA;IAED,OAAO,EAAE,MAAM,EAAE,0BAA0B,CAAC,MAAM,CAAC,EAAE,KAAK,EAAE,CAAA;AAC9D,CAAC,CAAA;AAED,iFAAiF;AACjF,MAAM,CAAC,MAAM,aAAa,GAAG,CAAC,IAAsB,EAAE,WAAmB,EAAW,EAAE,CACpF,IAAI,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,IAAI,MAAM,CAAC,aAAa,CAAC,WAAW,CAAC;OAC9D,WAAW,IAAI,IAAI,CAAC,MAAM,CAAC,YAAY,IAAI,WAAW,IAAI,IAAI,CAAC,MAAM,CAAC,YAAY,CAAA"}
@@ -0,0 +1,32 @@
1
+ import type { EntrypointOptions, EntrypointProtocol, OpenRequest, OpenValue } from '@owlmeans/entrypoint';
2
+ import type { RouteParent } from '@owlmeans/route';
3
+ import type { AmountPolicyQuery, AmountPolicyView, PlanPriceList, PlanPricesQuery } from '../types.js';
4
+ export declare const CHECKOUT_READ_API_PATH = "/checkout";
5
+ export interface CheckoutReadProtocolOptions {
6
+ /** Alias prefix of every declaration, e.g. `my-app:account:checkout`. */
7
+ prefix: string;
8
+ /** The application's guarded parent — its guards and gate are inherited. */
9
+ parent?: RouteParent;
10
+ guards?: EntrypointOptions['guards'];
11
+ gate?: EntrypointOptions['gate'];
12
+ /** Default `/checkout`. */
13
+ path?: string;
14
+ }
15
+ export type CheckoutReadProtocols = {
16
+ base: EntrypointProtocol<OpenRequest, OpenValue>;
17
+ /** The entity's amount policy as narrowed now — the same computation the checkout enforces. */
18
+ amountPolicy: EntrypointProtocol<{
19
+ query: AmountPolicyQuery;
20
+ }, AmountPolicyView>;
21
+ /** The prices a product's plans are charged at, per currency, as last synchronized. */
22
+ planPrices: EntrypointProtocol<{
23
+ query: PlanPricesQuery;
24
+ }, PlanPriceList>;
25
+ };
26
+ /**
27
+ * The read side of checkout: `amountPolicy` GET `/amount-policy?productSku&planSku` and
28
+ * `planPrices` GET `/plan-prices?productSku`, under the application's guarded `parent` (or its own
29
+ * `guards`). Aliases are `<prefix>:<name>`.
30
+ */
31
+ export declare const makeCheckoutReadProtocols: (opts: CheckoutReadProtocolOptions) => CheckoutReadProtocols;
32
+ //# sourceMappingURL=protocols.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"protocols.d.ts","sourceRoot":"","sources":["../../src/checkout/protocols.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAA;AAEzG,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAA;AAIlD,OAAO,KAAK,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;AAEtG,eAAO,MAAM,sBAAsB,cAAc,CAAA;AAEjD,MAAM,WAAW,2BAA2B;IAC1C,yEAAyE;IACzE,MAAM,EAAE,MAAM,CAAA;IACd,4EAA4E;IAC5E,MAAM,CAAC,EAAE,WAAW,CAAA;IACpB,MAAM,CAAC,EAAE,iBAAiB,CAAC,QAAQ,CAAC,CAAA;IACpC,IAAI,CAAC,EAAE,iBAAiB,CAAC,MAAM,CAAC,CAAA;IAChC,2BAA2B;IAC3B,IAAI,CAAC,EAAE,MAAM,CAAA;CACd;AAED,MAAM,MAAM,qBAAqB,GAAG;IAClC,IAAI,EAAE,kBAAkB,CAAC,WAAW,EAAE,SAAS,CAAC,CAAA;IAChD,+FAA+F;IAC/F,YAAY,EAAE,kBAAkB,CAAC;QAAE,KAAK,EAAE,iBAAiB,CAAA;KAAE,EAAE,gBAAgB,CAAC,CAAA;IAChF,uFAAuF;IACvF,UAAU,EAAE,kBAAkB,CAAC;QAAE,KAAK,EAAE,eAAe,CAAA;KAAE,EAAE,aAAa,CAAC,CAAA;CAC1E,CAAA;AAED;;;;GAIG;AACH,eAAO,MAAM,yBAAyB,SAAU,2BAA2B,KAAG,qBAwB7E,CAAA"}
@@ -0,0 +1,28 @@
1
+ import { contract, openProtocol, protocol, typed } from '@owlmeans/entrypoint';
2
+ import { backend, route, RouteMethod } from '@owlmeans/route';
3
+ import { AmountPolicyQuerySchema, AmountPolicyViewSchema, PlanPriceListSchema, PlanPricesQuerySchema, } from '../model/consumer.js';
4
+ export const CHECKOUT_READ_API_PATH = '/checkout';
5
+ /**
6
+ * The read side of checkout: `amountPolicy` GET `/amount-policy?productSku&planSku` and
7
+ * `planPrices` GET `/plan-prices?productSku`, under the application's guarded `parent` (or its own
8
+ * `guards`). Aliases are `<prefix>:<name>`.
9
+ */
10
+ export const makeCheckoutReadProtocols = (opts) => {
11
+ if (opts.parent == null && opts.guards == null) {
12
+ throw new SyntaxError('checkout-read: the routes need either a parent or explicit guards');
13
+ }
14
+ const alias = (name) => `${opts.prefix}:${name}`;
15
+ const path = opts.path ?? CHECKOUT_READ_API_PATH;
16
+ const base = opts.parent != null
17
+ ? openProtocol(route(alias('base'), path, backend({ parent: opts.parent })))
18
+ : openProtocol(route(alias('base'), path, backend()), {
19
+ guards: opts.guards,
20
+ ...(opts.gate != null ? { gate: opts.gate } : {}),
21
+ });
22
+ return {
23
+ base,
24
+ amountPolicy: protocol(route(alias('amount-policy'), '/amount-policy', backend({ parent: base, method: RouteMethod.GET })), contract.request({ query: typed(AmountPolicyQuerySchema) }, AmountPolicyViewSchema)),
25
+ planPrices: protocol(route(alias('plan-prices'), '/plan-prices', backend({ parent: base, method: RouteMethod.GET })), contract.request({ query: typed(PlanPricesQuerySchema) }, PlanPriceListSchema)),
26
+ };
27
+ };
28
+ //# sourceMappingURL=protocols.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"protocols.js","sourceRoot":"","sources":["../../src/checkout/protocols.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,sBAAsB,CAAA;AAE9E,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAA;AAE7D,OAAO,EACL,uBAAuB,EAAE,sBAAsB,EAAE,mBAAmB,EAAE,qBAAqB,GAC5F,MAAM,sBAAsB,CAAA;AAG7B,MAAM,CAAC,MAAM,sBAAsB,GAAG,WAAW,CAAA;AAqBjD;;;;GAIG;AACH,MAAM,CAAC,MAAM,yBAAyB,GAAG,CAAC,IAAiC,EAAyB,EAAE;IACpG,IAAI,IAAI,CAAC,MAAM,IAAI,IAAI,IAAI,IAAI,CAAC,MAAM,IAAI,IAAI,EAAE,CAAC;QAC/C,MAAM,IAAI,WAAW,CAAC,mEAAmE,CAAC,CAAA;IAC5F,CAAC;IACD,MAAM,KAAK,GAAG,CAAC,IAAY,EAAU,EAAE,CAAC,GAAG,IAAI,CAAC,MAAM,IAAI,IAAI,EAAE,CAAA;IAChE,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,IAAI,sBAAsB,CAAA;IAChD,MAAM,IAAI,GAAG,IAAI,CAAC,MAAM,IAAI,IAAI;QAC9B,CAAC,CAAC,YAAY,CAAC,KAAK,CAAC,KAAK,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,OAAO,CAAC,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC;QAC5E,CAAC,CAAC,YAAY,CAAC,KAAK,CAAC,KAAK,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC,EAAE;YAClD,MAAM,EAAE,IAAI,CAAC,MAAM;YACnB,GAAG,CAAC,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SAClD,CAAC,CAAA;IAEN,OAAO;QACL,IAAI;QACJ,YAAY,EAAE,QAAQ,CACpB,KAAK,CAAC,KAAK,CAAC,eAAe,CAAC,EAAE,gBAAgB,EAAE,OAAO,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,WAAW,CAAC,GAAG,EAAE,CAAC,CAAC,EACnG,QAAQ,CAAC,OAAO,CAAC,EAAE,KAAK,EAAE,KAAK,CAAoB,uBAAuB,CAAC,EAAE,EAAE,sBAAsB,CAAC,CACvG;QACD,UAAU,EAAE,QAAQ,CAClB,KAAK,CAAC,KAAK,CAAC,aAAa,CAAC,EAAE,cAAc,EAAE,OAAO,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,WAAW,CAAC,GAAG,EAAE,CAAC,CAAC,EAC/F,QAAQ,CAAC,OAAO,CAAC,EAAE,KAAK,EAAE,KAAK,CAAkB,qBAAqB,CAAC,EAAE,EAAE,mBAAmB,CAAC,CAChG;KACF,CAAA;AACH,CAAC,CAAA"}