@saasicat/nest 1.0.0-rc.2 → 1.0.0-rc.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +44 -8
- package/dist/.build-stamp +1 -1
- package/dist/_entries.cjs +10214 -4796
- package/dist/admin/index.d.cts +5 -4
- package/dist/admin/index.d.ts +5 -4
- package/dist/admin/index.js +13 -9
- package/dist/{admin-resources.module-CpvzcfCc.d.cts → admin-resources.module-B4XNvdH-.d.cts} +1 -2
- package/dist/{admin-resources.module-DF5_Q7RD.d.ts → admin-resources.module-CWqdI9BV.d.ts} +1 -2
- package/dist/{admin-stats.service-TYiSUwIR.d.ts → admin-stats.service-CsLNt19Z.d.ts} +11 -7
- package/dist/{admin-stats.service-DfKupywR.d.cts → admin-stats.service-DrZrU8IJ.d.cts} +11 -7
- package/dist/{aggregation-B3CD0v_d.d.cts → aggregation-ChpZDh0g.d.ts} +39 -6
- package/dist/{aggregation-BFzW07DE.d.ts → aggregation-OpnhwzF5.d.cts} +39 -6
- package/dist/billing/index.d.cts +448 -108
- package/dist/billing/index.d.ts +448 -108
- package/dist/billing/index.js +108 -41
- package/dist/catalog/index.d.cts +74 -45
- package/dist/catalog/index.d.ts +74 -45
- package/dist/catalog/index.js +21 -13
- package/dist/catalog.module-C9O7BxRq.d.ts +180 -0
- package/dist/catalog.module-T-I5Ricz.d.cts +180 -0
- package/dist/checkout-offer/index.d.cts +17 -22
- package/dist/checkout-offer/index.d.ts +17 -22
- package/dist/checkout-offer/index.js +21 -7
- package/dist/checkout-offer.module-DFhGy6JO.d.cts +58 -0
- package/dist/checkout-offer.module-x7SP_dX4.d.ts +58 -0
- package/dist/checkout-offer.service-BeHgYikI.d.ts +242 -0
- package/dist/checkout-offer.service-CCVxG9b2.d.cts +242 -0
- package/dist/chunk-34XVPR6P.js +222 -0
- package/dist/{chunk-AOQJEYOL.js → chunk-3QVGA6JX.js} +3 -1
- package/dist/chunk-3RLWML2S.js +896 -0
- package/dist/{chunk-TXC3LHHB.js → chunk-4SDSYV4A.js} +21 -13
- package/dist/chunk-55QEDAMX.js +946 -0
- package/dist/chunk-5CKC7O52.js +140 -0
- package/dist/chunk-6PJE7EWO.js +185 -0
- package/dist/chunk-7NJKVC5X.js +353 -0
- package/dist/{chunk-RUNZ3X4C.js → chunk-AOS2JPDD.js} +1263 -185
- package/dist/{chunk-WOVQPXV4.js → chunk-CQ2ZZMTD.js} +702 -725
- package/dist/{chunk-NA6O63B7.js → chunk-F5X66HF5.js} +5 -5
- package/dist/chunk-FEQZCWUL.js +174 -0
- package/dist/{chunk-VIJ5NJWC.js → chunk-G6EZWECL.js} +107 -31
- package/dist/chunk-G7RQO2XO.js +0 -0
- package/dist/{chunk-N3L3AICU.js → chunk-GPQWGA6B.js} +5 -6
- package/dist/chunk-GZTL64QK.js +7 -0
- package/dist/{chunk-SABTXESR.js → chunk-J7NJRK3K.js} +17 -1
- package/dist/{chunk-SEPN52AD.js → chunk-JBCC6C3M.js} +566 -307
- package/dist/{chunk-I7GTA3RX.js → chunk-JQRA724W.js} +1 -1
- package/dist/{chunk-WXYJHZCN.js → chunk-JVAEKTJ4.js} +2 -14
- package/dist/chunk-KPEMTBOP.js +26 -0
- package/dist/chunk-LF6J4YYN.js +301 -0
- package/dist/{chunk-LTT736P3.js → chunk-LLYVYRGJ.js} +228 -387
- package/dist/{chunk-6Z7JR4EW.js → chunk-M47BNEY2.js} +1897 -633
- package/dist/{chunk-KFT5AIIH.js → chunk-NDLC5GYK.js} +206 -24
- package/dist/{chunk-O2J2HDXA.js → chunk-OOTEXP47.js} +3 -8
- package/dist/chunk-QPVDCKYS.js +702 -0
- package/dist/{chunk-NHVDCYK5.js → chunk-R5YCJBHY.js} +71 -106
- package/dist/{chunk-XBYAFEOR.js → chunk-S33SO5XX.js} +1 -1
- package/dist/chunk-SZ7RFPXA.js +10 -0
- package/dist/chunk-TBBZWZQT.js +98 -0
- package/dist/chunk-TOEFN7DN.js +295 -0
- package/dist/{chunk-7ZEGFL42.js → chunk-WHJSYKNC.js} +6 -6
- package/dist/chunk-WOEJ6K7M.js +55 -0
- package/dist/{chunk-AU3OOREM.js → chunk-XSSWYPP5.js} +16 -3
- package/dist/contract-line-item-money-B0Z3Ogur.d.cts +43 -0
- package/dist/contract-line-item-money-B0Z3Ogur.d.ts +43 -0
- package/dist/discovery/index.d.cts +2 -2
- package/dist/discovery/index.d.ts +2 -2
- package/dist/discovery/index.js +6 -6
- package/dist/{discovery.scanner-9cMqF95o.d.cts → discovery.scanner-DXKc6JkV.d.cts} +1 -1
- package/dist/{discovery.scanner-9cMqF95o.d.ts → discovery.scanner-DXKc6JkV.d.ts} +1 -1
- package/dist/{enforce-quota.interceptor-Df0e3OWe.d.cts → enforce-quota.interceptor-BFyVdWe8.d.cts} +6 -6
- package/dist/{enforce-quota.interceptor-DW3etVyI.d.ts → enforce-quota.interceptor-GaBXboGx.d.ts} +6 -6
- package/dist/entitlement/index.d.cts +27 -4
- package/dist/entitlement/index.d.ts +27 -4
- package/dist/entitlement/index.js +13 -8
- package/dist/index.d.cts +209 -28
- package/dist/index.d.ts +209 -28
- package/dist/index.js +285 -163
- package/dist/issuer-identity.check-CBO0vcsq.d.cts +197 -0
- package/dist/issuer-identity.check-Cz9CEMkZ.d.ts +197 -0
- package/dist/{module-options-COSEb0VW.d.ts → module-options-DmQ3G4sZ.d.ts} +135 -41
- package/dist/{module-options-BG3MzQva.d.cts → module-options-qAOwinHR.d.cts} +135 -41
- package/dist/payment-callback.service-CR84Xw1Z.d.cts +125 -0
- package/dist/payment-callback.service-CR84Xw1Z.d.ts +125 -0
- package/dist/payments/index.cjs +4 -0
- package/dist/payments/index.d.cts +187 -0
- package/dist/payments/index.d.ts +187 -0
- package/dist/payments/index.js +141 -0
- package/dist/payments.module-BdUq9ot_.d.cts +57 -0
- package/dist/payments.module-CQGv_VG7.d.ts +57 -0
- package/dist/plan-catalog-source-DWe-BGY1.d.cts +21 -0
- package/dist/plan-catalog-source-DWe-BGY1.d.ts +21 -0
- package/dist/{plan-resolution-Cgo_TR1H.d.cts → plan-resolution-CwUy_OqC.d.cts} +14 -0
- package/dist/{plan-resolution-Cgo_TR1H.d.ts → plan-resolution-CwUy_OqC.d.ts} +14 -0
- package/dist/{plan-versions.service-DChhbK6h.d.ts → plan-versions.service-C0N-Em2W.d.ts} +38 -64
- package/dist/{plan-versions.service-TPEtbN0r.d.cts → plan-versions.service-DARLh-qI.d.cts} +38 -64
- package/dist/platform/index.d.cts +52 -23
- package/dist/platform/index.d.ts +52 -23
- package/dist/platform/index.js +57 -36
- package/dist/promo/index.d.cts +23 -8
- package/dist/promo/index.d.ts +23 -8
- package/dist/promo/index.js +15 -7
- package/dist/{promo.module-DtvIycpt.d.cts → promo.module-CKkECmU8.d.cts} +9 -3
- package/dist/{promo.module-Z8h1PNX-.d.ts → promo.module-DuIJdK3U.d.ts} +9 -3
- package/dist/promo.service-BYBu6dy3.d.cts +215 -0
- package/dist/promo.service-CFjfF0Xm.d.ts +215 -0
- package/dist/registration/index.d.cts +104 -47
- package/dist/registration/index.d.ts +104 -47
- package/dist/registration/index.js +15 -9
- package/dist/subscriber/index.cjs +4 -0
- package/dist/subscriber/index.d.cts +28 -0
- package/dist/subscriber/index.d.ts +28 -0
- package/dist/subscriber/index.js +16 -0
- package/dist/subscriber.service-5W6Gi-1B.d.cts +68 -0
- package/dist/subscriber.service-5W6Gi-1B.d.ts +68 -0
- package/dist/subscription-contract/index.d.cts +4 -2
- package/dist/subscription-contract/index.d.ts +4 -2
- package/dist/subscription-contract/index.js +13 -5
- package/dist/{subscription-contract.module-fDFdQ0jm.d.ts → subscription-contract.module-BfIRpc1o.d.ts} +8 -2
- package/dist/{subscription-contract.module-CEK8HS5-.d.cts → subscription-contract.module-DTMuUJYd.d.cts} +8 -2
- package/dist/{subscription-contract.service-hD87MKyg.d.cts → subscription-contract.service-CPZrTM9C.d.ts} +32 -3
- package/dist/{subscription-contract.service-hD87MKyg.d.ts → subscription-contract.service-CfNApLuP.d.cts} +32 -3
- package/dist/tenant-billing.controller-BLqbsONe.d.ts +812 -0
- package/dist/tenant-billing.controller-DKbAqTbM.d.cts +812 -0
- package/dist/tenant-billing.module-ClbjzocC.d.cts +364 -0
- package/dist/tenant-billing.module-DohgJJ2m.d.ts +364 -0
- package/dist/tenant-billing.tokens-G-1xOlMr.d.cts +129 -0
- package/dist/tenant-billing.tokens-G-1xOlMr.d.ts +129 -0
- package/dist/testing/index.d.cts +44 -17
- package/dist/testing/index.d.ts +44 -17
- package/dist/testing/index.js +189 -40
- package/package.json +24 -4
- package/dist/catalog.module-BDDO6iwq.d.cts +0 -103
- package/dist/catalog.module-BTnMsE6u.d.ts +0 -103
- package/dist/checkout-offer.module-CMtiQJTT.d.ts +0 -39
- package/dist/checkout-offer.module-CVpWrzbe.d.cts +0 -39
- package/dist/checkout-offer.service-BLOv2HOo.d.cts +0 -49
- package/dist/checkout-offer.service-BLOv2HOo.d.ts +0 -49
- package/dist/chunk-57V6ZTI6.js +0 -22
- package/dist/chunk-BSK6YBLI.js +0 -87
- package/dist/chunk-DQKCK7DX.js +0 -217
- package/dist/chunk-VXEYLNIB.js +0 -632
- package/dist/chunk-WUDYIPYH.js +0 -715
- package/dist/define-saasicat-DROBe-b1.d.ts +0 -69
- package/dist/define-saasicat-vGBCkFys.d.cts +0 -69
- package/dist/promo.service-BJbKAmw3.d.cts +0 -112
- package/dist/promo.service-BJbKAmw3.d.ts +0 -112
- package/dist/tenant-billing.controller-BQ67mlmI.d.ts +0 -438
- package/dist/tenant-billing.controller-DS8kB8a7.d.cts +0 -438
- package/dist/tenant-billing.module-BznC3XtW.d.cts +0 -299
- package/dist/tenant-billing.module-wUbsQSFG.d.ts +0 -299
- /package/dist/{chunk-2FR6ZL7R.js → chunk-BHZOH2DY.js} +0 -0
- /package/dist/{chunk-2SLTRKXC.js → chunk-DFOW3JVO.js} +0 -0
- /package/dist/{chunk-DBZGV3HC.js → chunk-DU56UKEH.js} +0 -0
- /package/dist/{chunk-OYNHOY45.js → chunk-GAUEVIAE.js} +0 -0
|
@@ -0,0 +1,812 @@
|
|
|
1
|
+
import { CanActivate, ExecutionContext } from '@nestjs/common';
|
|
2
|
+
import { d as AuthGuardList, a as TrialProjectionPort, P as PendingPlanQueryPort, T as TenantIdResolver, b as UserIdResolver, U as UserEmailResolver, A as AuditContextResolver } from './tenant-billing.tokens-G-1xOlMr.cjs';
|
|
3
|
+
import { BillingCycle, CancellationNoticePeriods, SubscriptionUsagePort, UsageSnapshotPort, SelfServiceBlockedPlans, SubscriptionBundleRepository, TenantSubscriptionWritePort, BundleRepository, SubscriptionBundleView, SubscriptionBundleRecord, PlanRepository, CatalogEntryRepository, SubscriptionUsageRecord, OnboardingSelectionResponse } from '@saasicat/core';
|
|
4
|
+
import { c as EntitlementService, a as EffectiveLimitsSnapshot, t as toEffectiveLimitsSnapshot } from './aggregation-OpnhwzF5.cjs';
|
|
5
|
+
import { d as ContractFreezePort, j as SelfServiceBlockedBundles } from './tenant-billing.module-ClbjzocC.cjs';
|
|
6
|
+
import { P as PlanCatalogSource } from './plan-catalog-source-DWe-BGY1.cjs';
|
|
7
|
+
import { a as PromoCodesService } from './promo.service-BYBu6dy3.cjs';
|
|
8
|
+
import { A as AdminAuditService } from './admin-audit.service-4P3Djdxk.cjs';
|
|
9
|
+
|
|
10
|
+
interface CancellationInput {
|
|
11
|
+
/** When the customer declared it. */
|
|
12
|
+
now: Date;
|
|
13
|
+
/** End of the period they are in, if the subscription has one. */
|
|
14
|
+
currentPeriodEnd: Date | null;
|
|
15
|
+
/** End of what was committed to. Null on a subscription with no term. */
|
|
16
|
+
minimumTermUntil: Date | null;
|
|
17
|
+
/** Needed only to find the following period end when notice has passed. */
|
|
18
|
+
billingCycle: BillingCycle;
|
|
19
|
+
/** Days before the term end after which a cancellation is too late. */
|
|
20
|
+
noticePeriodDays: number;
|
|
21
|
+
/**
|
|
22
|
+
* The day of the month the subscription is billed on.
|
|
23
|
+
*
|
|
24
|
+
* Only the hard cut reads it, and only then does it matter — but there it
|
|
25
|
+
* matters in money. A declaration made after the notice window lands one
|
|
26
|
+
* period past the term end, and computing that step from the term end alone
|
|
27
|
+
* takes its day from a date that may already have been clamped: an
|
|
28
|
+
* anchor-31 subscription whose term ends 28 February was cut to 28 March
|
|
29
|
+
* rather than 31 March, three days short of the period the customer had
|
|
30
|
+
* just been charged for.
|
|
31
|
+
*/
|
|
32
|
+
billingAnchorDay?: number | null;
|
|
33
|
+
}
|
|
34
|
+
interface CancellationDecision {
|
|
35
|
+
/** When the cancellation takes effect. */
|
|
36
|
+
effectiveAt: Date;
|
|
37
|
+
/** The term end it was measured against. */
|
|
38
|
+
termEndsAt: Date;
|
|
39
|
+
/** True when the notice window had already closed. */
|
|
40
|
+
afterNoticeDeadline: boolean;
|
|
41
|
+
/** The moment after which a cancellation lands one period later. */
|
|
42
|
+
noticeDeadline: Date | null;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Decides when a cancellation declared at `now` takes effect.
|
|
46
|
+
*
|
|
47
|
+
* Never returns a date in the past: a term that has already ended means the
|
|
48
|
+
* cancellation lands immediately, which is what a customer outside any
|
|
49
|
+
* commitment should get.
|
|
50
|
+
*/
|
|
51
|
+
declare function decideCancellation(input: CancellationInput): CancellationDecision;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* No notice at all, named so it is not a bare `{}` at a call site.
|
|
55
|
+
*
|
|
56
|
+
* Not a configuration default: `config/saas.yaml` requires both rhythms and an
|
|
57
|
+
* application that omits them does not boot. This is what lets a class be
|
|
58
|
+
* constructed directly — in a test that does not exercise a notice period —
|
|
59
|
+
* without restating a term. `TenantBillingModule` always provides the real one.
|
|
60
|
+
*/
|
|
61
|
+
declare const NO_NOTICE_PERIOD: CancellationNoticePeriods;
|
|
62
|
+
/**
|
|
63
|
+
* The notice a subscription on `billingCycle` is owed.
|
|
64
|
+
*
|
|
65
|
+
* The two rhythms are read apart rather than one falling back to the other:
|
|
66
|
+
* they are separate numbers because real contracts set them apart, and
|
|
67
|
+
* inferring one from the other would be inventing a term.
|
|
68
|
+
*
|
|
69
|
+
* `config/saas.yaml` requires both, so there is nothing to default here. A
|
|
70
|
+
* number that is not written down is not a zero — it is a question the operator
|
|
71
|
+
* has to answer before the application starts.
|
|
72
|
+
*/
|
|
73
|
+
declare function noticeDaysFor(periods: CancellationNoticePeriods, billingCycle: string): number;
|
|
74
|
+
/**
|
|
75
|
+
* The fields a cancellation is decided from. Narrow on purpose: this file
|
|
76
|
+
* decides dates and knows nothing about persistence.
|
|
77
|
+
*/
|
|
78
|
+
interface CancellableSubscription {
|
|
79
|
+
status: string;
|
|
80
|
+
billingCycle: BillingCycle;
|
|
81
|
+
currentPeriodEnd: Date | null;
|
|
82
|
+
minimumTermUntil: Date | null;
|
|
83
|
+
trialEndsAt: Date | null;
|
|
84
|
+
/** See `CancellationInput.billingAnchorDay`. */
|
|
85
|
+
billingAnchorDay?: number | null;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Decides a cancellation from a subscription, which is the only correct way to
|
|
89
|
+
* build the input above: reading the four date fields at a call site loses the
|
|
90
|
+
* fifth fact, and there is no type that notices.
|
|
91
|
+
*
|
|
92
|
+
* That fifth fact is whether the subscription commits to anything. A trial does
|
|
93
|
+
* not. It has a period end like every other subscription — the repository's own
|
|
94
|
+
* fixture sets one — and treating that as a term makes the two rules a customer
|
|
95
|
+
* meets disagree: a plan change during a trial takes effect at once because
|
|
96
|
+
* there is nothing to protect, while a cancellation was measured against a term
|
|
97
|
+
* that does not exist.
|
|
98
|
+
*
|
|
99
|
+
* The consequence was not a date a few weeks out. With a notice period
|
|
100
|
+
* configured, a trial ending in five days is already past its deadline, so the
|
|
101
|
+
* cancellation landed one BILLING CYCLE after the trial — a customer ending a
|
|
102
|
+
* yearly-cycle trial bought a year by cancelling it. The window exists so that a
|
|
103
|
+
* term cannot be left at the last moment; a trial has no term to leave.
|
|
104
|
+
*
|
|
105
|
+
* What a trial does have is an end, and the cancellation lands there: nothing is
|
|
106
|
+
* billed for the remainder and the customer keeps what they were given. Ending
|
|
107
|
+
* it on the spot would take the trial away as the price of saying they do not
|
|
108
|
+
* want to convert.
|
|
109
|
+
*/
|
|
110
|
+
declare function decideCancellationFor(sub: CancellableSubscription, now: Date, noticePeriodDays: number): CancellationDecision;
|
|
111
|
+
|
|
112
|
+
declare class ComposedTenantAuthGuard implements CanActivate {
|
|
113
|
+
private readonly guards;
|
|
114
|
+
constructor(guards?: AuthGuardList | null);
|
|
115
|
+
canActivate(context: ExecutionContext): Promise<boolean>;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
declare class TenantAdminGuard implements CanActivate {
|
|
119
|
+
canActivate(context: ExecutionContext): boolean;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
interface ProrationDto {
|
|
123
|
+
daysRemainingInPeriod: number;
|
|
124
|
+
daysInPeriod: number;
|
|
125
|
+
periodStart: Date;
|
|
126
|
+
periodEnd: Date;
|
|
127
|
+
currentPriceNet: number;
|
|
128
|
+
targetPriceNet: number;
|
|
129
|
+
/**
|
|
130
|
+
* What the change costs for the rest of the period, never below zero.
|
|
131
|
+
*
|
|
132
|
+
* The raw arithmetic goes negative when the target is cheaper than what is
|
|
133
|
+
* running — after a price reduction, an upgrade can arrive at a negative
|
|
134
|
+
* number. That is not a credit: this platform does not pay money back, and
|
|
135
|
+
* a negative charge carried into an invoice is a refund nobody agreed to.
|
|
136
|
+
* It is a free upgrade, and `isFree` is how a page says so.
|
|
137
|
+
*/
|
|
138
|
+
prorataDeltaNet: number;
|
|
139
|
+
/**
|
|
140
|
+
* The unclamped result, kept because the page has something to say about it.
|
|
141
|
+
*
|
|
142
|
+
* Dropping it would make "free" indistinguishable from "costs exactly
|
|
143
|
+
* nothing", and those read differently to someone deciding.
|
|
144
|
+
*/
|
|
145
|
+
rawDeltaNet: number;
|
|
146
|
+
/**
|
|
147
|
+
* True when the arithmetic asked for less than nothing.
|
|
148
|
+
*
|
|
149
|
+
* Strictly less: a change that costs exactly zero — equal prices, or a
|
|
150
|
+
* remaining fraction that rounds the difference away — is not a free
|
|
151
|
+
* upgrade, it is a change with no price difference. `rawDeltaNet` exists
|
|
152
|
+
* above precisely so a page can tell those apart, and a flag that merges
|
|
153
|
+
* them takes that back.
|
|
154
|
+
*/
|
|
155
|
+
isFree: boolean;
|
|
156
|
+
}
|
|
157
|
+
interface ProrationInput {
|
|
158
|
+
periodStart: Date;
|
|
159
|
+
periodEnd: Date;
|
|
160
|
+
now: Date;
|
|
161
|
+
/** Previous period price (bundle add: 0 — something is only added). */
|
|
162
|
+
currentPriceNet: number;
|
|
163
|
+
targetPriceNet: number;
|
|
164
|
+
}
|
|
165
|
+
declare function computeProration(input: ProrationInput): ProrationDto;
|
|
166
|
+
|
|
167
|
+
type PlanChangeType = 'UPGRADE' | 'DOWNGRADE' | 'CYCLE_CHANGE' | 'NOOP';
|
|
168
|
+
/** Where the target plan sits against the running one in the catalog order. */
|
|
169
|
+
type PlanDirection = 'UP' | 'DOWN' | 'SAME';
|
|
170
|
+
/**
|
|
171
|
+
* Whether the target billing period is longer, shorter or the same.
|
|
172
|
+
*
|
|
173
|
+
* Its own answer, deliberately. `changeType` collapses "a better plan" and "a
|
|
174
|
+
* shorter commitment" into one word, and the two have opposite consequences: a
|
|
175
|
+
* higher plan may start today, a shorter period may not — it would end a term
|
|
176
|
+
* the customer is still inside. Asked as one question, moving from a yearly
|
|
177
|
+
* STARTER to a monthly PRO reads as `UPGRADE`, applies immediately, and ends
|
|
178
|
+
* the yearly commitment early. That is the case this split exists for.
|
|
179
|
+
*/
|
|
180
|
+
type CycleDirection = 'LONGER' | 'SHORTER' | 'SAME';
|
|
181
|
+
interface PlanSnapshotDto {
|
|
182
|
+
id: string;
|
|
183
|
+
name: string;
|
|
184
|
+
monthlyNet: number | null;
|
|
185
|
+
yearlyNet: number | null;
|
|
186
|
+
quotas: Record<string, number>;
|
|
187
|
+
features: string[];
|
|
188
|
+
}
|
|
189
|
+
interface LimitsCheckRow {
|
|
190
|
+
used: number;
|
|
191
|
+
currentMax: number;
|
|
192
|
+
targetMax: number;
|
|
193
|
+
exceeded: boolean;
|
|
194
|
+
}
|
|
195
|
+
interface PlanChangePreviewIssue {
|
|
196
|
+
code: string;
|
|
197
|
+
/**
|
|
198
|
+
* English display text, and the last rung of the ladder rather than the
|
|
199
|
+
* first: `resolveErrorMessage` prefers the reader's catalogue and reaches
|
|
200
|
+
* this only for a code nobody has translated. It stays on the wire because
|
|
201
|
+
* a blocker that renders as an empty line leaves someone with a disabled
|
|
202
|
+
* button and no reason.
|
|
203
|
+
*/
|
|
204
|
+
message: string;
|
|
205
|
+
/**
|
|
206
|
+
* The values the sentence needs, beside it rather than inside it.
|
|
207
|
+
*
|
|
208
|
+
* Without these a client holding the code still cannot rebuild the
|
|
209
|
+
* sentence — it would have to parse English prose for the numbers. Every
|
|
210
|
+
* template in the shipped catalogues names only what appears here, and
|
|
211
|
+
* `preview-issues-are-translatable.test.js` holds that.
|
|
212
|
+
*/
|
|
213
|
+
params?: Record<string, string | number>;
|
|
214
|
+
}
|
|
215
|
+
interface PlanChangePreviewDto {
|
|
216
|
+
changeType: PlanChangeType;
|
|
217
|
+
/**
|
|
218
|
+
* The two answers `changeType` collapses into one.
|
|
219
|
+
*
|
|
220
|
+
* A page needs them apart to explain a deferred upgrade: the plan went up,
|
|
221
|
+
* the period got shorter, and it is the second that decided the date.
|
|
222
|
+
*/
|
|
223
|
+
planDirection: PlanDirection;
|
|
224
|
+
cycleDirection: CycleDirection;
|
|
225
|
+
current: {
|
|
226
|
+
plan: PlanSnapshotDto;
|
|
227
|
+
billingCycle: string;
|
|
228
|
+
};
|
|
229
|
+
target: {
|
|
230
|
+
plan: PlanSnapshotDto;
|
|
231
|
+
billingCycle: string;
|
|
232
|
+
};
|
|
233
|
+
/** For upgrade/NOOP: immediately (null). Otherwise period end. */
|
|
234
|
+
effectiveAt: Date | null;
|
|
235
|
+
isImmediate: boolean;
|
|
236
|
+
/**
|
|
237
|
+
* Projected new trial end after the change (app trial logic, e.g.
|
|
238
|
+
* carry-over of the remaining time). `null` if no TrialProjectionPort is
|
|
239
|
+
* configured, the subscription is not in a trial, or nothing changes.
|
|
240
|
+
* The wizard uses this to show "regular from the end of the trial".
|
|
241
|
+
*/
|
|
242
|
+
projectedTrialEndsAt: Date | null;
|
|
243
|
+
proration: ProrationDto | null;
|
|
244
|
+
/** Map quotaKey → LimitsCheckRow across all quota dimensions from the current limit, target plan and usage. */
|
|
245
|
+
limitsCheck: Record<string, LimitsCheckRow>;
|
|
246
|
+
featuresLost: string[];
|
|
247
|
+
featuresGained: string[];
|
|
248
|
+
/** Hard prevention reasons — e.g. usage > target limit. */
|
|
249
|
+
blockers: PlanChangePreviewIssue[];
|
|
250
|
+
/** Non-blocking hints — e.g. feature loss. */
|
|
251
|
+
warnings: PlanChangePreviewIssue[];
|
|
252
|
+
}
|
|
253
|
+
interface PlanChangeContext {
|
|
254
|
+
/** Current period start time from the subscription, if present. */
|
|
255
|
+
currentPeriodStart: Date | null;
|
|
256
|
+
/** Current period end from the subscription, if present. */
|
|
257
|
+
currentPeriodEnd: Date | null;
|
|
258
|
+
/**
|
|
259
|
+
* End of what was committed to, which can outlast the period.
|
|
260
|
+
*
|
|
261
|
+
* They coincide until a notice period pushes one past the other. A change
|
|
262
|
+
* scheduled to the period end alone would then materialise inside the
|
|
263
|
+
* commitment this rule exists to protect — the customer keeps the plan they
|
|
264
|
+
* are bound to for eleven months and loses it in the twelfth.
|
|
265
|
+
*/
|
|
266
|
+
minimumTermUntil: Date | null;
|
|
267
|
+
/** TRIAL end, if status === 'TRIAL'. */
|
|
268
|
+
trialEndsAt: Date | null;
|
|
269
|
+
/**
|
|
270
|
+
* The cancellation, because it decides what a change may still do.
|
|
271
|
+
*
|
|
272
|
+
* A cancellation was measured against the term of the cycle it was declared
|
|
273
|
+
* under, so that cycle cannot move while it is outstanding. The route
|
|
274
|
+
* refuses such a change; without the same answer here, a reader is walked
|
|
275
|
+
* through the whole wizard — the acknowledgement included — and meets the
|
|
276
|
+
* refusal only when they press confirm.
|
|
277
|
+
*/
|
|
278
|
+
canceledAt: Date | null;
|
|
279
|
+
canceledEffectiveAt: Date | null;
|
|
280
|
+
/** Subscription status (TRIAL/ACTIVE/...). */
|
|
281
|
+
status: string;
|
|
282
|
+
/** Current cycle of the subscription (for cycle-change classification). */
|
|
283
|
+
currentBillingCycle: string;
|
|
284
|
+
/** Current plan of the subscription. */
|
|
285
|
+
currentPlan: string;
|
|
286
|
+
/** Subscription start, if present (for the periodEndAfter fallback). */
|
|
287
|
+
startedAt: Date | null;
|
|
288
|
+
}
|
|
289
|
+
declare class PlanChangePreviewService {
|
|
290
|
+
private readonly catalogs;
|
|
291
|
+
private readonly entitlements;
|
|
292
|
+
private readonly subscriptions;
|
|
293
|
+
private readonly usageSnapshot;
|
|
294
|
+
private readonly blockedPlans;
|
|
295
|
+
private readonly trialProjection;
|
|
296
|
+
private readonly subscriptionBundles;
|
|
297
|
+
constructor(catalogs: PlanCatalogSource, entitlements: EntitlementService, subscriptions: SubscriptionUsagePort, usageSnapshot: UsageSnapshotPort, blockedPlans?: SelfServiceBlockedPlans | null, trialProjection?: TrialProjectionPort | null, subscriptionBundles?: SubscriptionBundleRepository | null);
|
|
298
|
+
preview(tenantId: string, targetPlan: string, targetCycle: string, now?: Date): Promise<PlanChangePreviewDto>;
|
|
299
|
+
/** Like `preview`, but only the blocker list — for a server-side
|
|
300
|
+
* pre-check before the `changePlan` mutation (defense-in-depth). */
|
|
301
|
+
assertChangeAllowed(tenantId: string, targetPlan: string, targetCycle: string, now?: Date): Promise<PlanChangePreviewIssue[]>;
|
|
302
|
+
/** Catalog order decides which plan is higher; equal keys are `SAME`. */
|
|
303
|
+
private planDirection;
|
|
304
|
+
/**
|
|
305
|
+
* YEARLY is the longer commitment; anything else is compared against it.
|
|
306
|
+
*
|
|
307
|
+
* Written as a comparison rather than a pair of equality checks so a third
|
|
308
|
+
* cycle — quarterly is the one that keeps being asked for — orders itself
|
|
309
|
+
* instead of falling into `SAME` and quietly becoming immediate.
|
|
310
|
+
*/
|
|
311
|
+
private cycleDirection;
|
|
312
|
+
/**
|
|
313
|
+
* Active bookings whose own rhythm would not fit `targetCycle`.
|
|
314
|
+
*
|
|
315
|
+
* Reads the booking's stored rhythm, not the plan's: a booking with none
|
|
316
|
+
* follows the plan and therefore fits any plan by construction. Empty
|
|
317
|
+
* without the bundle module, which is a consumer that has no bookings at
|
|
318
|
+
* all rather than one whose bookings are being ignored.
|
|
319
|
+
*/
|
|
320
|
+
private bookingsOutlastingCycle;
|
|
321
|
+
private classify;
|
|
322
|
+
/** Catalog order = rank. Non-marketed plans go to the end. */
|
|
323
|
+
private planRank;
|
|
324
|
+
private resolveEffectiveAt;
|
|
325
|
+
private computeProration;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
declare class PendingPlanMaterializationService {
|
|
329
|
+
private readonly query;
|
|
330
|
+
private readonly subscriptionWrite;
|
|
331
|
+
private readonly entitlements;
|
|
332
|
+
private readonly contractFreeze;
|
|
333
|
+
private readonly logger;
|
|
334
|
+
constructor(query: PendingPlanQueryPort, subscriptionWrite: TenantSubscriptionWritePort, entitlements: EntitlementService, contractFreeze?: ContractFreezePort | null);
|
|
335
|
+
materializeDuePlanChanges(now?: Date): Promise<{
|
|
336
|
+
applied: number;
|
|
337
|
+
}>;
|
|
338
|
+
private tryFreeze;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
declare class PreviewPlanChangeDto {
|
|
342
|
+
plan: string;
|
|
343
|
+
billingCycle: string;
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* A plan change carries what to change to, never when.
|
|
347
|
+
*
|
|
348
|
+
* It used to carry `effectiveImmediately`, and the route honoured it — so a
|
|
349
|
+
* direct call could take the immediate path and end a term the customer was
|
|
350
|
+
* inside. When a change lands follows from the plan direction, the cycle
|
|
351
|
+
* direction and the minimum term, all of which the server knows and the caller
|
|
352
|
+
* does not.
|
|
353
|
+
*/
|
|
354
|
+
declare class ChangePlanDto extends PreviewPlanChangeDto {
|
|
355
|
+
}
|
|
356
|
+
/**
|
|
357
|
+
* A cancellation carries nothing.
|
|
358
|
+
*
|
|
359
|
+
* It used to carry `immediately`, and honouring it let a tenant end a term they
|
|
360
|
+
* were still inside — the one thing this route may not do. A cancellation is a
|
|
361
|
+
* declaration; when it lands is decided from the minimum term and the notice
|
|
362
|
+
* period, not asked for. Ending a contract on the spot is an operator's act and
|
|
363
|
+
* goes through the operator's own path.
|
|
364
|
+
*
|
|
365
|
+
* Kept as an empty class rather than deleted so `whitelist` still strips a body
|
|
366
|
+
* from a client that has not been updated, instead of the field reaching a
|
|
367
|
+
* handler that would ignore it silently.
|
|
368
|
+
*/
|
|
369
|
+
declare class CancelSubscriptionDto {
|
|
370
|
+
/**
|
|
371
|
+
* The effective date the page showed before the customer confirmed.
|
|
372
|
+
*
|
|
373
|
+
* Optional, and checked rather than trusted: if the server's own decision
|
|
374
|
+
* differs, the request is refused with the new date so the page can ask
|
|
375
|
+
* again. The window is small and the consequence is not — a dialog opened
|
|
376
|
+
* before a notice deadline and confirmed after it promises January 2027 and
|
|
377
|
+
* would deliver January 2028.
|
|
378
|
+
*
|
|
379
|
+
* This is the caller telling the server what only the caller knows: what
|
|
380
|
+
* the human actually agreed to.
|
|
381
|
+
*/
|
|
382
|
+
expectedEffectiveAt?: string;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
declare class CompleteOnboardingSubscriptionDto {
|
|
386
|
+
plan: string;
|
|
387
|
+
billingCycle: string;
|
|
388
|
+
promoCode?: string;
|
|
389
|
+
/**
|
|
390
|
+
* Optional — UUIDs of the BundleVersions that should be booked
|
|
391
|
+
* together with the plan (P11.7.3). Per bundle, the platform
|
|
392
|
+
* default minimum term (none) is set. Bundles are added
|
|
393
|
+
* best-effort **after** the plan change — an error on an individual
|
|
394
|
+
* bundle (e.g. incompatible with the chosen plan) lands as a
|
|
395
|
+
* warning in the response, without rolling back the plan change.
|
|
396
|
+
*/
|
|
397
|
+
bundleVersionIds?: string[];
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
interface SubscriptionBundleConfig {
|
|
401
|
+
/**
|
|
402
|
+
* Default minimum term in months on `add`. Default = 0 — no commitment,
|
|
403
|
+
* so an add-on can be cancelled to its own period end. An operator who
|
|
404
|
+
* wants one configures it.
|
|
405
|
+
*/
|
|
406
|
+
defaultMinimumTermMonths?: number;
|
|
407
|
+
}
|
|
408
|
+
interface AddBundleToSubscriptionInput {
|
|
409
|
+
subscriptionId: string;
|
|
410
|
+
bundleVersionId: string;
|
|
411
|
+
/** PlanKey of the current subscription for the plan-compatibility check. */
|
|
412
|
+
currentPlanKey: string;
|
|
413
|
+
/** Default = now (service time). */
|
|
414
|
+
startedAt?: Date;
|
|
415
|
+
/**
|
|
416
|
+
* Override for the minimum term (months). Default = config or
|
|
417
|
+
* 12. `0` explicitly means "no minimum term"
|
|
418
|
+
* (`minimumTermEndsAt = null`).
|
|
419
|
+
*/
|
|
420
|
+
minimumTermMonths?: number;
|
|
421
|
+
/**
|
|
422
|
+
* When the parent subscription ends, or null while it runs on.
|
|
423
|
+
*
|
|
424
|
+
* A bundle cannot commit past the subscription that pays for it. With a
|
|
425
|
+
* cancellation outstanding, a twelve-month default term on a subscription
|
|
426
|
+
* ending in three weeks binds a customer to something that has three weeks
|
|
427
|
+
* left to give — and once the parent ends, entitlement resolution grants
|
|
428
|
+
* nothing through it.
|
|
429
|
+
*
|
|
430
|
+
* Required rather than optional: a caller that omits it commits the
|
|
431
|
+
* customer for longer than the contract can deliver, and the omission is
|
|
432
|
+
* invisible.
|
|
433
|
+
*/
|
|
434
|
+
parentEndsAt: Date | null;
|
|
435
|
+
/**
|
|
436
|
+
* The plan's rhythm, its current period end, and the day it is billed on.
|
|
437
|
+
*
|
|
438
|
+
* A bundle runs in step with the plan that pays for it: its periods end on
|
|
439
|
+
* the plan's day, and its first one is short. Aligned here, at booking,
|
|
440
|
+
* rather than trimmed when the plan ends — a trim means somebody was
|
|
441
|
+
* committed to more than they received, and then owed the difference.
|
|
442
|
+
*
|
|
443
|
+
* The plan's cycle is also the ceiling for the bundle's: a bundle may run
|
|
444
|
+
* in a shorter rhythm, never a longer one.
|
|
445
|
+
*/
|
|
446
|
+
planCycle: BillingCycle;
|
|
447
|
+
planPeriodEnd: Date | null;
|
|
448
|
+
planAnchorDay: number | null;
|
|
449
|
+
/** The bundle's own rhythm. Defaults to the plan's. */
|
|
450
|
+
billingCycle?: BillingCycle;
|
|
451
|
+
}
|
|
452
|
+
interface CancelBundleFromSubscriptionInput {
|
|
453
|
+
subscriptionBundleId: string;
|
|
454
|
+
/** Default = now. */
|
|
455
|
+
canceledAt?: Date;
|
|
456
|
+
/**
|
|
457
|
+
* Period end of the subscription from which the cancellation could take effect.
|
|
458
|
+
* Effective date = `max(currentPeriodEnd, minimumTermEndsAt)`.
|
|
459
|
+
* If not set, `canceledAt` is interpreted as the period end
|
|
460
|
+
* (= immediate effect, provided the minimum term has already expired).
|
|
461
|
+
*/
|
|
462
|
+
currentPeriodEnd?: Date;
|
|
463
|
+
/**
|
|
464
|
+
* When the parent subscription ends, or null while it runs on.
|
|
465
|
+
*
|
|
466
|
+
* A bundle cannot be held past the plan that pays for it, and the term was
|
|
467
|
+
* written at booking — before any cancellation declared since. Reading the
|
|
468
|
+
* boundary here is what makes that harmless: no clamp at insert time can
|
|
469
|
+
* see a cancellation that had not happened yet.
|
|
470
|
+
*/
|
|
471
|
+
parentEndsAt: Date | null;
|
|
472
|
+
}
|
|
473
|
+
declare class SubscriptionBundlesService {
|
|
474
|
+
private readonly repo;
|
|
475
|
+
private readonly bundles;
|
|
476
|
+
private readonly blockedBundles;
|
|
477
|
+
private readonly defaultMinTermMonths;
|
|
478
|
+
constructor(repo: SubscriptionBundleRepository, bundles: BundleRepository, config?: SubscriptionBundleConfig, blockedBundles?: SelfServiceBlockedBundles | null);
|
|
479
|
+
/** All bundle bookings of a subscription (for the "My Bundles" page). */
|
|
480
|
+
/**
|
|
481
|
+
* The tenant's bookings, with the price each one is actually billed at.
|
|
482
|
+
*
|
|
483
|
+
* `planKey` is required because a price is not a property of a bundle
|
|
484
|
+
* alone: a `BundlePricingOverride` can set a different one per plan, and
|
|
485
|
+
* the rhythm decides which of the two figures applies. Returning the base
|
|
486
|
+
* monthly price regardless — which this did until 2026-08-27 — puts a
|
|
487
|
+
* number on the tenant's screen that nobody is charged, and on a yearly
|
|
488
|
+
* booking it was out by whatever the yearly price is.
|
|
489
|
+
*/
|
|
490
|
+
listForSubscription(subscriptionId: string, planKey: string, planCycle: string): Promise<SubscriptionBundleView[]>;
|
|
491
|
+
/**
|
|
492
|
+
* List prices for the given bundle versions, in both rhythms, resolved for
|
|
493
|
+
* one plan.
|
|
494
|
+
*
|
|
495
|
+
* The public catalogue cannot answer this: it has no tenant and therefore
|
|
496
|
+
* no plan, so it serves the base prices and a bundle priced only through an
|
|
497
|
+
* override reads as having no price at all. A tenant UI that treated those
|
|
498
|
+
* fields as final hid such a bundle behind "not available in this rhythm"
|
|
499
|
+
* while the booking would have gone through.
|
|
500
|
+
*/
|
|
501
|
+
resolvePricesFor(planKey: string, bundleVersionIds: string[]): Promise<Record<string, {
|
|
502
|
+
monthlyNet: number | null;
|
|
503
|
+
yearlyNet: number | null;
|
|
504
|
+
}>>;
|
|
505
|
+
addBundleToSubscription(input: AddBundleToSubscriptionInput): Promise<SubscriptionBundleRecord>;
|
|
506
|
+
cancelBundleFromSubscription(input: CancelBundleFromSubscriptionInput): Promise<SubscriptionBundleRecord>;
|
|
507
|
+
/**
|
|
508
|
+
* "Undo cancellation" — only as long as the cancellation is not yet effective
|
|
509
|
+
* (the bundle runs until `canceledEffectiveAt`). After that, re-booking is the way.
|
|
510
|
+
*/
|
|
511
|
+
reactivateBundle(subscriptionBundleId: string): Promise<SubscriptionBundleRecord>;
|
|
512
|
+
}
|
|
513
|
+
/**
|
|
514
|
+
* Effective date of a bundle cancellation:
|
|
515
|
+
* `max(currentPeriodEnd, minimumTermEndsAt)` — missing values fall back to
|
|
516
|
+
* `canceledAt` (= immediate effect). Shared between the
|
|
517
|
+
* cancellation mutation and the preview (#37).
|
|
518
|
+
*/
|
|
519
|
+
declare function resolveBundleCancelEffectiveAt(input: {
|
|
520
|
+
canceledAt: Date;
|
|
521
|
+
currentPeriodEnd: Date | null;
|
|
522
|
+
minimumTermEndsAt: Date | null;
|
|
523
|
+
/** When the parent subscription ends, or null while it runs on. */
|
|
524
|
+
parentEndsAt: Date | null;
|
|
525
|
+
}): Date;
|
|
526
|
+
/**
|
|
527
|
+
* Adds `months` to `date` and keeps the UTC day. Edge case
|
|
528
|
+
* 31.01 + 1 month → 28/29.02 (JS Date does this automatically by
|
|
529
|
+
* setMonth normalizing the day).
|
|
530
|
+
*/
|
|
531
|
+
declare function addMonths(date: Date, months: number): Date;
|
|
532
|
+
declare function clampToParent(ownTermEndsAt: Date | null, parentEndsAt: Date | null): Date | null;
|
|
533
|
+
|
|
534
|
+
interface SubscriptionBundlePreviewIssue {
|
|
535
|
+
code: string;
|
|
536
|
+
message: string;
|
|
537
|
+
/**
|
|
538
|
+
* The values the sentence names, beside it rather than inside it — the same
|
|
539
|
+
* contract `PlanChangePreviewIssue` carries, so one consumer mechanism
|
|
540
|
+
* resolves the issues of both previews.
|
|
541
|
+
*/
|
|
542
|
+
params?: Record<string, string | number>;
|
|
543
|
+
}
|
|
544
|
+
/** Subscription context — the controller reads it from the SubscriptionUsagePort. */
|
|
545
|
+
interface SubscriptionBundlePreviewContext {
|
|
546
|
+
subscriptionId: string;
|
|
547
|
+
/** PlanKey of the current subscription (plan compatibility + redundancy source). */
|
|
548
|
+
currentPlanKey: string;
|
|
549
|
+
/** 'MONTHLY' | 'YEARLY' (port convention). */
|
|
550
|
+
billingCycle: string;
|
|
551
|
+
/** Subscription status (TRIAL/ACTIVE/...). No proration during TRIAL. */
|
|
552
|
+
status: string;
|
|
553
|
+
startedAt: Date | null;
|
|
554
|
+
currentPeriodStart: Date | null;
|
|
555
|
+
currentPeriodEnd: Date | null;
|
|
556
|
+
/**
|
|
557
|
+
* When the parent subscription ends, or null while it runs on.
|
|
558
|
+
*
|
|
559
|
+
* The dialog states the date the booking will be committed to, and the
|
|
560
|
+
* mutation caps that date at the parent's end. A preview that does not cap
|
|
561
|
+
* it describes a different contract from the one that is written.
|
|
562
|
+
*/
|
|
563
|
+
parentEndsAt: Date | null;
|
|
564
|
+
/**
|
|
565
|
+
* The day of the month the plan is billed on, where it is known.
|
|
566
|
+
*
|
|
567
|
+
* The bundle's periods land on this day, so the preview needs the same
|
|
568
|
+
* value the booking uses. Reading it from the plan's period end instead
|
|
569
|
+
* would hand on a date a short month has already clamped, and the quoted
|
|
570
|
+
* first period would then differ from the one written.
|
|
571
|
+
*/
|
|
572
|
+
planAnchorDay?: number | null;
|
|
573
|
+
}
|
|
574
|
+
interface BundlePreviewSnapshot {
|
|
575
|
+
bundleKey: string;
|
|
576
|
+
label: string;
|
|
577
|
+
bundleVersionId: string;
|
|
578
|
+
features: string[];
|
|
579
|
+
quotas: Record<string, number>;
|
|
580
|
+
}
|
|
581
|
+
/** AK-13: feature is already paid for elsewhere — double-payment hint. */
|
|
582
|
+
interface RedundantFeatureHint {
|
|
583
|
+
featureKey: string;
|
|
584
|
+
coveredBy: 'PLAN' | 'BUNDLE';
|
|
585
|
+
/** planKey or bundleKey of the covering source. */
|
|
586
|
+
coveredByKey: string;
|
|
587
|
+
}
|
|
588
|
+
interface SubscriptionBundleAddPreviewDto {
|
|
589
|
+
action: 'add';
|
|
590
|
+
bundle: BundlePreviewSnapshot;
|
|
591
|
+
billingCycle: string;
|
|
592
|
+
/**
|
|
593
|
+
* Prorated amount until period end. `null` during TRIAL (no paid
|
|
594
|
+
* period yet) or without a list price for the cycle.
|
|
595
|
+
*/
|
|
596
|
+
proration: ProrationDto | null;
|
|
597
|
+
/** List price per follow-up period in the current cycle; null = no price maintained. */
|
|
598
|
+
nextPeriodPriceNet: number | null;
|
|
599
|
+
minimumTermMonths: number;
|
|
600
|
+
/** Projected minimum-term end from `now`; null = no minimum term. */
|
|
601
|
+
minimumTermEndsAt: Date | null;
|
|
602
|
+
/**
|
|
603
|
+
* End of the first billing period, on the plan's billing day.
|
|
604
|
+
*
|
|
605
|
+
* Shorter than a full cycle in the usual case, and charged pro rata for
|
|
606
|
+
* exactly that stretch (`proration`). Null where the plan has no period to
|
|
607
|
+
* align to — a trial, or a subscription not yet started.
|
|
608
|
+
*/
|
|
609
|
+
firstPeriodEnd: Date | null;
|
|
610
|
+
/**
|
|
611
|
+
* The day the bundle ends because the plan does, or null while the plan
|
|
612
|
+
* runs on.
|
|
613
|
+
*
|
|
614
|
+
* Ending with the plan is not a cancellation: no notice is needed, and the
|
|
615
|
+
* period the bundle is in when it happens is not credited. The alignment
|
|
616
|
+
* exists so that day is a period boundary — this field is what remains
|
|
617
|
+
* when the plan is already ending on a day the bundle has been paid past.
|
|
618
|
+
*/
|
|
619
|
+
endsWithPlanAt: Date | null;
|
|
620
|
+
redundantFeatures: RedundantFeatureHint[];
|
|
621
|
+
/**
|
|
622
|
+
* requires-features (#35) that neither the plan nor active bundles nor the
|
|
623
|
+
* bundle itself cover. Non-empty ⇒ blocker
|
|
624
|
+
* BUNDLE_FEATURE_DEPENDENCY_UNSATISFIED.
|
|
625
|
+
*/
|
|
626
|
+
missingRequires: string[];
|
|
627
|
+
blockers: SubscriptionBundlePreviewIssue[];
|
|
628
|
+
warnings: SubscriptionBundlePreviewIssue[];
|
|
629
|
+
}
|
|
630
|
+
interface SubscriptionBundleCancelPreviewDto {
|
|
631
|
+
action: 'cancel';
|
|
632
|
+
subscriptionBundleId: string;
|
|
633
|
+
bundle: BundlePreviewSnapshot;
|
|
634
|
+
billingCycle: string;
|
|
635
|
+
/** Effective date = max(currentPeriodEnd, minimumTermEndsAt). */
|
|
636
|
+
effectiveAt: Date;
|
|
637
|
+
/** Savings per period from the effective date; null = no price maintained. */
|
|
638
|
+
nextPeriodSavingsNet: number | null;
|
|
639
|
+
blockers: SubscriptionBundlePreviewIssue[];
|
|
640
|
+
warnings: SubscriptionBundlePreviewIssue[];
|
|
641
|
+
}
|
|
642
|
+
declare class SubscriptionBundlePreviewService {
|
|
643
|
+
private readonly subscriptionBundles;
|
|
644
|
+
private readonly bundles;
|
|
645
|
+
private readonly plans;
|
|
646
|
+
private readonly catalogEntries;
|
|
647
|
+
private readonly blockedBundles;
|
|
648
|
+
private readonly defaultMinTermMonths;
|
|
649
|
+
constructor(subscriptionBundles: SubscriptionBundleRepository, bundles: BundleRepository, plans?: PlanRepository | null, catalogEntries?: CatalogEntryRepository | null, blockedBundles?: SelfServiceBlockedBundles | null, config?: SubscriptionBundleConfig);
|
|
650
|
+
previewAdd(ctx: SubscriptionBundlePreviewContext, input: {
|
|
651
|
+
bundleVersionId: string;
|
|
652
|
+
minimumTermMonths?: number;
|
|
653
|
+
/** The bundle's own rhythm. Defaults to the plan's, as the booking does. */
|
|
654
|
+
billingCycle?: BillingCycle;
|
|
655
|
+
}, now?: Date): Promise<SubscriptionBundleAddPreviewDto>;
|
|
656
|
+
previewCancel(ctx: SubscriptionBundlePreviewContext, input: {
|
|
657
|
+
subscriptionBundleId: string;
|
|
658
|
+
}, now?: Date): Promise<SubscriptionBundleCancelPreviewDto>;
|
|
659
|
+
/** Bookability checks — same codes as `addBundleToSubscription` (422 path). */
|
|
660
|
+
private collectBookabilityBlockers;
|
|
661
|
+
/** Versions of the active bundle bookings (for redundancy + requires coverage). */
|
|
662
|
+
private loadActiveBundleVersions;
|
|
663
|
+
/** Features of the currently live PlanVersion state; empty without PlanRepository. */
|
|
664
|
+
private resolvePlanFeatures;
|
|
665
|
+
private collectRedundantFeatures;
|
|
666
|
+
/**
|
|
667
|
+
* requires of the new bundle that neither the bundle itself nor plan ∪
|
|
668
|
+
* active bundles cover (#35). Empty without CatalogEntryRepository.
|
|
669
|
+
*/
|
|
670
|
+
private collectMissingRequires;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
interface RequestLike {
|
|
674
|
+
user?: {
|
|
675
|
+
tenantId?: string;
|
|
676
|
+
sub?: string;
|
|
677
|
+
id?: string;
|
|
678
|
+
};
|
|
679
|
+
}
|
|
680
|
+
interface UsageResponse {
|
|
681
|
+
plan: string;
|
|
682
|
+
effectivePlan: string;
|
|
683
|
+
billingCycle: string;
|
|
684
|
+
status: string;
|
|
685
|
+
isPilot: boolean;
|
|
686
|
+
pilotEndsAt: Date | null;
|
|
687
|
+
trialEndsAt: Date | null;
|
|
688
|
+
startedAt: Date | null;
|
|
689
|
+
currentPeriodStart: Date | null;
|
|
690
|
+
currentPeriodEnd: Date | null;
|
|
691
|
+
pendingPlan: string | null;
|
|
692
|
+
pendingBillingCycle: string | null;
|
|
693
|
+
pendingEffectiveAt: Date | null;
|
|
694
|
+
planVersion: SubscriptionUsageRecord['planVersion'];
|
|
695
|
+
pendingPlanVersion: SubscriptionUsageRecord['pendingPlanVersion'];
|
|
696
|
+
pendingPlanVersionEffectiveAt: Date | null;
|
|
697
|
+
pendingPlanVersionAccepted: boolean;
|
|
698
|
+
pendingPlanVersionAcceptedAt: Date | null;
|
|
699
|
+
/** When a cancellation was declared. Null while none was. */
|
|
700
|
+
canceledAt: Date | null;
|
|
701
|
+
/** When it lands. A tenant keeps everything until then. */
|
|
702
|
+
canceledEffectiveAt: Date | null;
|
|
703
|
+
/**
|
|
704
|
+
* What cancelling right now would do — the date, and why it is that date.
|
|
705
|
+
*
|
|
706
|
+
* A projection, not a state: it is recomputed on every read, and it says
|
|
707
|
+
* nothing about whether a cancellation exists. `canceledEffectiveAt` above
|
|
708
|
+
* is the one that does.
|
|
709
|
+
*/
|
|
710
|
+
cancellation: CancellationDecision;
|
|
711
|
+
limits: ReturnType<typeof toEffectiveLimitsSnapshot>;
|
|
712
|
+
usage: Record<string, number>;
|
|
713
|
+
/**
|
|
714
|
+
* P11.4: Frozen package snapshot from the
|
|
715
|
+
* `CheckoutOffer` that was activated during onboarding. Read-only
|
|
716
|
+
* for the tenant self-service UI. `null` for subscriptions without
|
|
717
|
+
* a CheckoutOffer origin.
|
|
718
|
+
*/
|
|
719
|
+
packageSnapshot: unknown | null;
|
|
720
|
+
/** P11.4: Optional reference to the originating CheckoutOffer. */
|
|
721
|
+
checkoutOfferId: string | null;
|
|
722
|
+
}
|
|
723
|
+
declare class TenantBillingController {
|
|
724
|
+
private readonly entitlements;
|
|
725
|
+
private readonly planPreview;
|
|
726
|
+
private readonly subscriptionUsage;
|
|
727
|
+
private readonly usageSnapshot;
|
|
728
|
+
private readonly subscriptionWrite;
|
|
729
|
+
private readonly tenantIdResolver;
|
|
730
|
+
private readonly userIdResolver;
|
|
731
|
+
private readonly blockedPlans;
|
|
732
|
+
private readonly promoCodes;
|
|
733
|
+
private readonly auditService;
|
|
734
|
+
private readonly userEmailResolver;
|
|
735
|
+
private readonly auditContextResolver;
|
|
736
|
+
private readonly subscriptionBundles;
|
|
737
|
+
private readonly contractFreeze;
|
|
738
|
+
private readonly trialProjection;
|
|
739
|
+
private readonly cancellationNoticeDays;
|
|
740
|
+
constructor(entitlements: EntitlementService, planPreview: PlanChangePreviewService, subscriptionUsage: SubscriptionUsagePort, usageSnapshot: UsageSnapshotPort, subscriptionWrite: TenantSubscriptionWritePort, tenantIdResolver?: TenantIdResolver | null, userIdResolver?: UserIdResolver | null, blockedPlans?: SelfServiceBlockedPlans | null, promoCodes?: PromoCodesService | null, auditService?: AdminAuditService | null, userEmailResolver?: UserEmailResolver | null, auditContextResolver?: AuditContextResolver | null, subscriptionBundles?: SubscriptionBundlesService | null, contractFreeze?: ContractFreezePort | null, trialProjection?: TrialProjectionPort | null, cancellationNoticeDays?: CancellationNoticePeriods);
|
|
741
|
+
private readonly logger;
|
|
742
|
+
getEntitlement(req: RequestLike): Promise<EffectiveLimitsSnapshot>;
|
|
743
|
+
getUsage(req: RequestLike): Promise<UsageResponse>;
|
|
744
|
+
previewPlanChange(req: RequestLike, dto: PreviewPlanChangeDto): Promise<PlanChangePreviewDto>;
|
|
745
|
+
changePlan(req: RequestLike, dto: ChangePlanDto): Promise<{
|
|
746
|
+
plan: string;
|
|
747
|
+
billingCycle: string;
|
|
748
|
+
immediate: boolean;
|
|
749
|
+
pendingPlan?: undefined;
|
|
750
|
+
pendingBillingCycle?: undefined;
|
|
751
|
+
pendingEffectiveAt?: undefined;
|
|
752
|
+
} | {
|
|
753
|
+
plan: string;
|
|
754
|
+
billingCycle: string;
|
|
755
|
+
pendingPlan: string;
|
|
756
|
+
pendingBillingCycle: string;
|
|
757
|
+
pendingEffectiveAt: Date;
|
|
758
|
+
immediate: boolean;
|
|
759
|
+
}>;
|
|
760
|
+
completeOnboardingSubscription(req: RequestLike, dto: CompleteOnboardingSubscriptionDto): Promise<OnboardingSelectionResponse>;
|
|
761
|
+
acceptPendingPlanVersion(req: RequestLike): Promise<{
|
|
762
|
+
accepted: boolean;
|
|
763
|
+
acceptedAt: Date | null;
|
|
764
|
+
effectiveAt: Date | null;
|
|
765
|
+
idempotent: boolean;
|
|
766
|
+
}>;
|
|
767
|
+
cancelSubscription(req: RequestLike, dto: CancelSubscriptionDto): Promise<{
|
|
768
|
+
canceledAt: Date | null;
|
|
769
|
+
canceledEffectiveAt: Date | null;
|
|
770
|
+
status: string;
|
|
771
|
+
termEndsAt: null;
|
|
772
|
+
noticeDeadline: null;
|
|
773
|
+
afterNoticeDeadline: null;
|
|
774
|
+
alreadyCanceled: boolean;
|
|
775
|
+
} | {
|
|
776
|
+
canceledAt: Date | null;
|
|
777
|
+
canceledEffectiveAt: Date | null;
|
|
778
|
+
status: string;
|
|
779
|
+
termEndsAt: Date;
|
|
780
|
+
noticeDeadline: Date | null;
|
|
781
|
+
afterNoticeDeadline: boolean;
|
|
782
|
+
alreadyCanceled: boolean;
|
|
783
|
+
}>;
|
|
784
|
+
/**
|
|
785
|
+
* What a cancellation declared at `now` would do. One method, because the
|
|
786
|
+
* page states the date and this route applies it, and two constructions of
|
|
787
|
+
* the same input are two chances to disagree about a rule the customer
|
|
788
|
+
* meets once.
|
|
789
|
+
*/
|
|
790
|
+
private projectCancellation;
|
|
791
|
+
private requireTenantId;
|
|
792
|
+
private requireUserId;
|
|
793
|
+
private buildActor;
|
|
794
|
+
private resolveUserEmail;
|
|
795
|
+
private toResponseRedemption;
|
|
796
|
+
private collectPromoSkipReasons;
|
|
797
|
+
/**
|
|
798
|
+
* #18: contract freeze after the plan change — non-fatal, only outside of
|
|
799
|
+
* the trial (during a trial the trial entitlements apply, not the booked plan;
|
|
800
|
+
* the freeze happens on the transition to ACTIVE, i.e. on materialization).
|
|
801
|
+
* Without a configured `contractFreeze` hook the call is a no-op.
|
|
802
|
+
*/
|
|
803
|
+
private tryFreezeOnPlanChange;
|
|
804
|
+
/**
|
|
805
|
+
* Audit-log helper — writes best-effort, does not block the response path.
|
|
806
|
+
* If `AdminAuditService` is not injected (e.g. a minimal deploy without
|
|
807
|
+
* AdminModule), the call is silently discarded.
|
|
808
|
+
*/
|
|
809
|
+
private auditLog;
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
export { type AddBundleToSubscriptionInput as A, type BundlePreviewSnapshot as B, type CancelBundleFromSubscriptionInput as C, clampToParent as D, computeProration as E, decideCancellation as F, decideCancellationFor as G, noticeDaysFor as H, resolveBundleCancelEffectiveAt as I, type LimitsCheckRow as L, NO_NOTICE_PERIOD as N, PendingPlanMaterializationService as P, type RedundantFeatureHint as R, type SubscriptionBundleAddPreviewDto as S, TenantAdminGuard as T, type UsageResponse as U, CancelSubscriptionDto as a, type CancellableSubscription as b, type CancellationDecision as c, type CancellationInput as d, ChangePlanDto as e, CompleteOnboardingSubscriptionDto as f, ComposedTenantAuthGuard as g, type CycleDirection as h, type PlanChangeContext as i, type PlanChangePreviewDto as j, type PlanChangePreviewIssue as k, PlanChangePreviewService as l, type PlanChangeType as m, type PlanDirection as n, type PlanSnapshotDto as o, PreviewPlanChangeDto as p, type ProrationDto as q, type ProrationInput as r, type SubscriptionBundleCancelPreviewDto as s, type SubscriptionBundleConfig as t, type SubscriptionBundlePreviewContext as u, type SubscriptionBundlePreviewIssue as v, SubscriptionBundlePreviewService as w, SubscriptionBundlesService as x, TenantBillingController as y, addMonths as z };
|