@saasicat/nest 0.18.0 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_entries.cjs +1309 -519
- package/dist/admin/index.js +1 -1
- package/dist/billing/index.d.cts +7 -4
- package/dist/billing/index.d.ts +7 -4
- package/dist/billing/index.js +6 -6
- package/dist/catalog/index.js +4 -4
- package/dist/checkout-offer/index.js +2 -2
- package/dist/{chunk-Z3FWN3HJ.js → chunk-5ZXBXYXU.js} +3 -3
- package/dist/{chunk-RZVSKKXX.js → chunk-6WA4X5U3.js} +46 -45
- package/dist/{chunk-V34AA7C7.js → chunk-6WL6VQ7Z.js} +1 -1
- package/dist/{chunk-ABB2CRAS.js → chunk-74SCJ4HF.js} +1 -1
- package/dist/{chunk-PLJEAUFU.js → chunk-7JIHG53V.js} +76 -39
- package/dist/{chunk-7RRWI5LH.js → chunk-AFK2IE2L.js} +10 -10
- package/dist/{chunk-32EMXPU2.js → chunk-B7NRY2LV.js} +48 -17
- package/dist/{chunk-MDIZUVIK.js → chunk-BSK6YBLI.js} +1 -1
- package/dist/{chunk-R2QOSVAA.js → chunk-DQKCK7DX.js} +1 -1
- package/dist/{chunk-GLLCB4BP.js → chunk-EUU3B6NB.js} +17 -13
- package/dist/{chunk-EXOLOJFZ.js → chunk-F6DCLVBB.js} +29 -8
- package/dist/{chunk-5KH2O4SF.js → chunk-I77VS3YH.js} +287 -105
- package/dist/{chunk-YITTE646.js → chunk-KUDKD7ZG.js} +34 -14
- package/dist/{chunk-P3DP6GJE.js → chunk-LXUBCLRK.js} +486 -160
- package/dist/{chunk-GJBXOCRZ.js → chunk-ORWIGSNN.js} +45 -17
- package/dist/{chunk-E7AJ42AB.js → chunk-QLJHIIOY.js} +168 -28
- package/dist/{chunk-BOADBIGB.js → chunk-YYUXKLXK.js} +2 -2
- package/dist/{define-saasicat-Rc0rXrYG.d.ts → define-saasicat-CqkhnHn-.d.ts} +1 -1
- package/dist/{define-saasicat-CeDBX9ji.d.cts → define-saasicat-D8c6Bs4T.d.cts} +1 -1
- package/dist/discovery/index.d.cts +2 -2
- package/dist/discovery/index.d.ts +2 -2
- package/dist/discovery/index.js +4 -4
- package/dist/{enforce-quota.interceptor-DMw-bPyk.d.cts → enforce-quota.interceptor-Dk0eEtzV.d.cts} +1 -1
- package/dist/{enforce-quota.interceptor-CmbWX39L.d.ts → enforce-quota.interceptor-Dp8wTYro.d.ts} +1 -1
- package/dist/entitlement/index.js +2 -2
- package/dist/index.d.cts +6 -6
- package/dist/index.d.ts +6 -6
- package/dist/index.js +17 -17
- package/dist/{module-DgYJur6f.d.ts → module-BwD-wAor.d.ts} +1 -1
- package/dist/{module-CSN8j8Tp.d.cts → module-DsDkbawm.d.cts} +1 -1
- package/dist/platform/index.d.cts +8 -8
- package/dist/platform/index.d.ts +8 -8
- package/dist/platform/index.js +13 -13
- package/dist/promo/index.d.cts +3 -3
- package/dist/promo/index.d.ts +3 -3
- package/dist/promo/index.js +3 -3
- package/dist/registration/index.js +1 -1
- package/dist/{saas-platform.module-DLhoqKNA.d.ts → saas-platform.module-BQmrDGVU.d.ts} +1 -1
- package/dist/{saas-platform.module-D3rUTiho.d.cts → saas-platform.module-DducnQ9s.d.cts} +1 -1
- package/dist/{service-DX8KbGXl.d.cts → service-DPZ2W1E-.d.cts} +2 -2
- package/dist/{service-DX8KbGXl.d.ts → service-DPZ2W1E-.d.ts} +2 -2
- package/dist/subscription-contract/index.js +2 -2
- package/dist/{tenant-billing.controller-BwZRnJ3q.d.ts → tenant-billing.controller-b1G1yB09.d.ts} +2 -2
- package/dist/{tenant-billing.controller-CG4CaFUP.d.cts → tenant-billing.controller-m42YQ_Ei.d.cts} +2 -2
- package/dist/testing/index.d.cts +5 -5
- package/dist/testing/index.d.ts +5 -5
- package/dist/testing/index.js +33 -33
- package/package.json +3 -3
package/dist/_entries.cjs
CHANGED
|
@@ -87,6 +87,7 @@ var RLS_BYPASS_PORT_TOKEN = /* @__PURE__ */ Symbol.for("saas-platform/RlsBypassP
|
|
|
87
87
|
|
|
88
88
|
// src/admin/super-admin.guard.ts
|
|
89
89
|
var import_common = require("@nestjs/common");
|
|
90
|
+
var import_types = require("@saasicat/types");
|
|
90
91
|
function _ts_decorate(decorators, target, key, desc) {
|
|
91
92
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
92
93
|
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
@@ -100,10 +101,18 @@ var SuperAdminGuard = class {
|
|
|
100
101
|
}
|
|
101
102
|
canActivate(context) {
|
|
102
103
|
const request = context.switchToHttp().getRequest();
|
|
103
|
-
if (!request.user)
|
|
104
|
+
if (!request.user) {
|
|
105
|
+
throw new import_common.ForbiddenException({
|
|
106
|
+
code: import_types.AUTH_ERROR_CODES.NOT_AUTHENTICATED,
|
|
107
|
+
message: "Not authenticated"
|
|
108
|
+
});
|
|
109
|
+
}
|
|
104
110
|
const role = request.user.platformRole ?? request.user.role;
|
|
105
111
|
if (role !== "SUPER_ADMIN") {
|
|
106
|
-
throw new import_common.ForbiddenException(
|
|
112
|
+
throw new import_common.ForbiddenException({
|
|
113
|
+
code: import_types.AUTH_ERROR_CODES.SUPER_ADMIN_REQUIRED,
|
|
114
|
+
message: "Only the SUPER_ADMIN role is allowed"
|
|
115
|
+
});
|
|
107
116
|
}
|
|
108
117
|
return true;
|
|
109
118
|
}
|
|
@@ -115,6 +124,7 @@ SuperAdminGuard = _ts_decorate([
|
|
|
115
124
|
// src/admin/mfa.guard.ts
|
|
116
125
|
var import_common3 = require("@nestjs/common");
|
|
117
126
|
var import_core = require("@nestjs/core");
|
|
127
|
+
var import_types2 = require("@saasicat/types");
|
|
118
128
|
|
|
119
129
|
// src/admin/mfa.ts
|
|
120
130
|
var import_common2 = require("@nestjs/common");
|
|
@@ -194,7 +204,7 @@ var MfaService = class _MfaService {
|
|
|
194
204
|
return false;
|
|
195
205
|
}
|
|
196
206
|
const message = error instanceof Error ? error.message : String(error);
|
|
197
|
-
this.logger.warn(`TOTP
|
|
207
|
+
this.logger.warn(`TOTP verification for user ${input.userId} failed unexpectedly: ${message}`);
|
|
198
208
|
return false;
|
|
199
209
|
}
|
|
200
210
|
}
|
|
@@ -260,22 +270,30 @@ var MfaGuard = class _MfaGuard {
|
|
|
260
270
|
}
|
|
261
271
|
const request = context.switchToHttp().getRequest();
|
|
262
272
|
const user = request.user;
|
|
263
|
-
if (!user)
|
|
264
|
-
|
|
265
|
-
|
|
273
|
+
if (!user) {
|
|
274
|
+
throw new import_common3.UnauthorizedException({
|
|
275
|
+
code: import_types2.AUTH_ERROR_CODES.NOT_AUTHENTICATED,
|
|
276
|
+
// `reason` is superseded by `code` and will be removed.
|
|
277
|
+
reason: "NOT_AUTHENTICATED"
|
|
278
|
+
});
|
|
279
|
+
}
|
|
266
280
|
const enabled = await this.mfaService.isEnabled(user.id);
|
|
267
281
|
if (!enabled) {
|
|
268
282
|
throw new import_common3.UnauthorizedException({
|
|
283
|
+
code: import_types2.AUTH_ERROR_CODES.MFA_NOT_SET_UP,
|
|
284
|
+
// `reason` is superseded by `code` and will be removed.
|
|
269
285
|
reason: "MFA_NOT_SET_UP",
|
|
270
|
-
message: "
|
|
286
|
+
message: "Run the MFA setup via the CLI first."
|
|
271
287
|
});
|
|
272
288
|
}
|
|
273
289
|
const headerVal = request.headers?.["x-mfa-code"];
|
|
274
290
|
const code = Array.isArray(headerVal) ? headerVal[0] : headerVal;
|
|
275
291
|
if (!code) {
|
|
276
292
|
throw new import_common3.UnauthorizedException({
|
|
293
|
+
code: import_types2.AUTH_ERROR_CODES.MFA_REQUIRED,
|
|
294
|
+
// `reason` is superseded by `code` and will be removed.
|
|
277
295
|
reason: "MFA_REQUIRED",
|
|
278
|
-
message: "TOTP
|
|
296
|
+
message: "TOTP code required in the X-Mfa-Code header."
|
|
279
297
|
});
|
|
280
298
|
}
|
|
281
299
|
const valid = await this.mfaService.verify({
|
|
@@ -284,8 +302,10 @@ var MfaGuard = class _MfaGuard {
|
|
|
284
302
|
});
|
|
285
303
|
if (!valid) {
|
|
286
304
|
throw new import_common3.UnauthorizedException({
|
|
305
|
+
code: import_types2.AUTH_ERROR_CODES.MFA_FAILED,
|
|
306
|
+
// `reason` is superseded by `code` and will be removed.
|
|
287
307
|
reason: "MFA_FAILED",
|
|
288
|
-
message: "TOTP
|
|
308
|
+
message: "Invalid TOTP code."
|
|
289
309
|
});
|
|
290
310
|
}
|
|
291
311
|
return true;
|
|
@@ -799,7 +819,7 @@ var PLATFORM_CORE_MANIFEST_CONTRIBUTION = {
|
|
|
799
819
|
actions: [
|
|
800
820
|
{
|
|
801
821
|
id: "platform.tenants.suspend",
|
|
802
|
-
label: "
|
|
822
|
+
label: "Suspend tenant",
|
|
803
823
|
actionKey: "tenants.suspend",
|
|
804
824
|
requiredCapability: "tenants.suspend",
|
|
805
825
|
requiresMfa: true,
|
|
@@ -807,7 +827,7 @@ var PLATFORM_CORE_MANIFEST_CONTRIBUTION = {
|
|
|
807
827
|
},
|
|
808
828
|
{
|
|
809
829
|
id: "platform.tenants.reactivate",
|
|
810
|
-
label: "
|
|
830
|
+
label: "Reactivate tenant",
|
|
811
831
|
actionKey: "tenants.reactivate",
|
|
812
832
|
requiredCapability: "tenants.reactivate",
|
|
813
833
|
requiresMfa: true,
|
|
@@ -823,7 +843,7 @@ var PLATFORM_CORE_MANIFEST_CONTRIBUTION = {
|
|
|
823
843
|
},
|
|
824
844
|
{
|
|
825
845
|
id: "platform.tenants.export",
|
|
826
|
-
label: "
|
|
846
|
+
label: "GDPR export",
|
|
827
847
|
actionKey: "tenants.export",
|
|
828
848
|
requiredCapability: "tenants.export",
|
|
829
849
|
requiresMfa: true,
|
|
@@ -831,7 +851,7 @@ var PLATFORM_CORE_MANIFEST_CONTRIBUTION = {
|
|
|
831
851
|
},
|
|
832
852
|
{
|
|
833
853
|
id: "platform.subscriptions.cancel",
|
|
834
|
-
label: "
|
|
854
|
+
label: "Cancel subscription",
|
|
835
855
|
actionKey: "subscriptions.cancel",
|
|
836
856
|
requiredCapability: "subscriptions.cancel",
|
|
837
857
|
requiresMfa: true,
|
|
@@ -839,7 +859,7 @@ var PLATFORM_CORE_MANIFEST_CONTRIBUTION = {
|
|
|
839
859
|
},
|
|
840
860
|
{
|
|
841
861
|
id: "platform.pilots.grant",
|
|
842
|
-
label: "
|
|
862
|
+
label: "Set pilot status",
|
|
843
863
|
actionKey: "pilots.grant",
|
|
844
864
|
requiredCapability: "pilots.grant",
|
|
845
865
|
requiresMfa: true,
|
|
@@ -847,7 +867,7 @@ var PLATFORM_CORE_MANIFEST_CONTRIBUTION = {
|
|
|
847
867
|
},
|
|
848
868
|
{
|
|
849
869
|
id: "platform.pilots.revoke",
|
|
850
|
-
label: "
|
|
870
|
+
label: "Revoke pilot status",
|
|
851
871
|
actionKey: "pilots.revoke",
|
|
852
872
|
requiredCapability: "pilots.revoke",
|
|
853
873
|
requiresMfa: true,
|
|
@@ -855,7 +875,7 @@ var PLATFORM_CORE_MANIFEST_CONTRIBUTION = {
|
|
|
855
875
|
},
|
|
856
876
|
{
|
|
857
877
|
id: "platform.pilots.extend",
|
|
858
|
-
label: "
|
|
878
|
+
label: "Extend pilot",
|
|
859
879
|
actionKey: "pilots.extend",
|
|
860
880
|
requiredCapability: "pilots.extend",
|
|
861
881
|
requiresMfa: true,
|
|
@@ -867,102 +887,102 @@ var PLATFORM_CORE_MANIFEST_CONTRIBUTION = {
|
|
|
867
887
|
actions: [
|
|
868
888
|
{
|
|
869
889
|
key: "TENANT_SUSPEND",
|
|
870
|
-
label: "
|
|
890
|
+
label: "Tenant suspended",
|
|
871
891
|
severity: "high"
|
|
872
892
|
},
|
|
873
893
|
{
|
|
874
894
|
key: "TENANT_REACTIVATE",
|
|
875
|
-
label: "
|
|
895
|
+
label: "Tenant reactivated",
|
|
876
896
|
severity: "medium"
|
|
877
897
|
},
|
|
878
898
|
{
|
|
879
899
|
key: "TENANT_IMPERSONATE",
|
|
880
|
-
label: "Impersonation
|
|
900
|
+
label: "Impersonation started",
|
|
881
901
|
severity: "high"
|
|
882
902
|
},
|
|
883
903
|
{
|
|
884
904
|
key: "TENANT_EXPORT",
|
|
885
|
-
label: "
|
|
905
|
+
label: "GDPR export triggered",
|
|
886
906
|
severity: "medium"
|
|
887
907
|
},
|
|
888
908
|
{
|
|
889
909
|
key: "PILOT_CREATE",
|
|
890
|
-
label: "Pilot
|
|
910
|
+
label: "Pilot tenant created",
|
|
891
911
|
severity: "medium"
|
|
892
912
|
},
|
|
893
913
|
{
|
|
894
914
|
key: "PILOT_GRANT",
|
|
895
|
-
label: "Pilot
|
|
915
|
+
label: "Pilot status set",
|
|
896
916
|
severity: "medium"
|
|
897
917
|
},
|
|
898
918
|
{
|
|
899
919
|
key: "PILOT_REVOKE",
|
|
900
|
-
label: "Pilot
|
|
920
|
+
label: "Pilot status revoked",
|
|
901
921
|
severity: "medium"
|
|
902
922
|
},
|
|
903
923
|
{
|
|
904
924
|
key: "PILOT_EXTEND",
|
|
905
|
-
label: "Pilot
|
|
925
|
+
label: "Pilot extended",
|
|
906
926
|
severity: "low"
|
|
907
927
|
},
|
|
908
928
|
{
|
|
909
929
|
key: "PLAN_VERSION_PUBLISH",
|
|
910
|
-
label: "Plan
|
|
930
|
+
label: "Plan version approved",
|
|
911
931
|
severity: "high"
|
|
912
932
|
},
|
|
913
933
|
{
|
|
914
934
|
key: "PLAN_VERSION_TERMINATE",
|
|
915
|
-
label: "Plan
|
|
935
|
+
label: "Plan version terminated",
|
|
916
936
|
severity: "high"
|
|
917
937
|
},
|
|
918
938
|
{
|
|
919
939
|
key: "SUBSCRIPTION_CANCEL",
|
|
920
|
-
label: "Subscription
|
|
940
|
+
label: "Subscription cancelled",
|
|
921
941
|
severity: "high"
|
|
922
942
|
},
|
|
923
943
|
{
|
|
924
944
|
key: "USER_RESET_PASSWORD",
|
|
925
|
-
label: "
|
|
945
|
+
label: "Password reset triggered",
|
|
926
946
|
severity: "medium"
|
|
927
947
|
},
|
|
928
948
|
{
|
|
929
949
|
key: "USER_DEACTIVATE",
|
|
930
|
-
label: "User
|
|
950
|
+
label: "User deactivated",
|
|
931
951
|
severity: "high"
|
|
932
952
|
},
|
|
933
953
|
{
|
|
934
954
|
key: "USER_REASSIGN_ADMIN",
|
|
935
|
-
label: "Admin
|
|
955
|
+
label: "Admin role transferred",
|
|
936
956
|
severity: "high"
|
|
937
957
|
},
|
|
938
958
|
{
|
|
939
959
|
key: "PLATFORM_EMAIL_PROVIDER_CREATE",
|
|
940
|
-
label: "
|
|
960
|
+
label: "Platform sender created",
|
|
941
961
|
severity: "medium"
|
|
942
962
|
},
|
|
943
963
|
{
|
|
944
964
|
key: "PLATFORM_EMAIL_PROVIDER_UPDATE",
|
|
945
|
-
label: "
|
|
965
|
+
label: "Platform sender changed",
|
|
946
966
|
severity: "medium"
|
|
947
967
|
},
|
|
948
968
|
{
|
|
949
969
|
key: "PLATFORM_EMAIL_PROVIDER_DELETE",
|
|
950
|
-
label: "
|
|
970
|
+
label: "Platform sender deleted",
|
|
951
971
|
severity: "medium"
|
|
952
972
|
},
|
|
953
973
|
{
|
|
954
974
|
key: "PLATFORM_EMAIL_PROVIDER_TEST",
|
|
955
|
-
label: "
|
|
975
|
+
label: "Platform sender test send",
|
|
956
976
|
severity: "low"
|
|
957
977
|
},
|
|
958
978
|
{
|
|
959
979
|
key: "EMAIL_HISTORY_DELETE",
|
|
960
|
-
label: "
|
|
980
|
+
label: "Platform email removed from history",
|
|
961
981
|
severity: "medium"
|
|
962
982
|
},
|
|
963
983
|
{
|
|
964
984
|
key: "EMAIL_HISTORY_RESEND",
|
|
965
|
-
label: "
|
|
985
|
+
label: "Platform email resent",
|
|
966
986
|
severity: "low"
|
|
967
987
|
}
|
|
968
988
|
]
|
|
@@ -1081,7 +1101,7 @@ var AdminManifestModule = class _AdminManifestModule {
|
|
|
1081
1101
|
];
|
|
1082
1102
|
if (includeManifestController) {
|
|
1083
1103
|
if (options.guards === void 0) {
|
|
1084
|
-
throw new Error("AdminManifestModule.forRoot: `guards`
|
|
1104
|
+
throw new Error("AdminManifestModule.forRoot: `guards` is required when `includeManifestController` is enabled (default `true`). Set `guards: [JwtAuthGuard, SuperAdminGuard]` (or equivalent) \u2014 or `guards: []` to deliberately leave the endpoint unauthenticated. Alternative: `includeManifestController: false` plus registering your own wrapping controller.");
|
|
1085
1105
|
}
|
|
1086
1106
|
controllers.push(buildManifestController(options.guards, options.reloadGuards ?? []));
|
|
1087
1107
|
}
|
|
@@ -1318,6 +1338,7 @@ AdminStatsModule = _ts_decorate12([
|
|
|
1318
1338
|
// src/admin/admin-resources.module.ts
|
|
1319
1339
|
var import_common14 = require("@nestjs/common");
|
|
1320
1340
|
var import_class_validator = require("class-validator");
|
|
1341
|
+
var import_types3 = require("@saasicat/types");
|
|
1321
1342
|
function _ts_decorate13(decorators, target, key, desc) {
|
|
1322
1343
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
1323
1344
|
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
@@ -1363,7 +1384,15 @@ var AdminResourcesService = class {
|
|
|
1363
1384
|
}
|
|
1364
1385
|
async getTenantDetail(slug) {
|
|
1365
1386
|
const tenant = await this.resources.getTenantDetail(slug);
|
|
1366
|
-
if (!tenant)
|
|
1387
|
+
if (!tenant) {
|
|
1388
|
+
throw new import_common14.NotFoundException({
|
|
1389
|
+
code: import_types3.BILLING_ERROR_CODES.TENANT_NOT_FOUND,
|
|
1390
|
+
message: `Tenant ${slug} not found`,
|
|
1391
|
+
params: {
|
|
1392
|
+
slug
|
|
1393
|
+
}
|
|
1394
|
+
});
|
|
1395
|
+
}
|
|
1367
1396
|
return tenant;
|
|
1368
1397
|
}
|
|
1369
1398
|
listUsers(filter) {
|
|
@@ -1389,7 +1418,15 @@ var AdminResourcesService = class {
|
|
|
1389
1418
|
}
|
|
1390
1419
|
async setTenantActive(slug, active, subscriptionStatus, actor, audit) {
|
|
1391
1420
|
const result = await this.resources.setTenantActive(slug, active, subscriptionStatus);
|
|
1392
|
-
if (!result)
|
|
1421
|
+
if (!result) {
|
|
1422
|
+
throw new import_common14.NotFoundException({
|
|
1423
|
+
code: import_types3.BILLING_ERROR_CODES.TENANT_NOT_FOUND,
|
|
1424
|
+
message: `Tenant ${slug} not found`,
|
|
1425
|
+
params: {
|
|
1426
|
+
slug
|
|
1427
|
+
}
|
|
1428
|
+
});
|
|
1429
|
+
}
|
|
1393
1430
|
await this.audit.log({
|
|
1394
1431
|
actor,
|
|
1395
1432
|
entity: "Tenant",
|
|
@@ -1631,8 +1668,8 @@ __export(billing_exports, {
|
|
|
1631
1668
|
buildPlanCatalogFromSnapshot: () => buildPlanCatalogFromSnapshot,
|
|
1632
1669
|
buildPlanCatalogImporterController: () => buildPlanCatalogImporterController,
|
|
1633
1670
|
buildTenantSubscriptionBundlesController: () => buildTenantSubscriptionBundlesController,
|
|
1634
|
-
classifyBundleVersionDiff: () =>
|
|
1635
|
-
classifyPlanDiff: () =>
|
|
1671
|
+
classifyBundleVersionDiff: () => import_types4.classifyBundleVersionDiff,
|
|
1672
|
+
classifyPlanDiff: () => import_types4.classifyPlanDiff,
|
|
1636
1673
|
clearPendingPlanVersionFields: () => clearPendingPlanVersionFields,
|
|
1637
1674
|
computeCarriedTrialEndsAt: () => computeCarriedTrialEndsAt,
|
|
1638
1675
|
computeNextPeriod: () => computeNextPeriod,
|
|
@@ -1693,7 +1730,7 @@ function periodEndWithMinLead(startedAt, cycle, now, minLeadDays = 42) {
|
|
|
1693
1730
|
__name(periodEndWithMinLead, "periodEndWithMinLead");
|
|
1694
1731
|
|
|
1695
1732
|
// src/billing/version-diff.ts
|
|
1696
|
-
var
|
|
1733
|
+
var import_types4 = require("@saasicat/types");
|
|
1697
1734
|
|
|
1698
1735
|
// src/billing/version-publish.ts
|
|
1699
1736
|
var PublishValidationError = class extends Error {
|
|
@@ -1709,35 +1746,35 @@ var PublishValidationError = class extends Error {
|
|
|
1709
1746
|
function assertChangeNote(changeNote) {
|
|
1710
1747
|
const trimmed = (changeNote ?? "").trim();
|
|
1711
1748
|
if (trimmed.length === 0) {
|
|
1712
|
-
throw new PublishValidationError("CHANGE_NOTE_REQUIRED", "changeNote
|
|
1749
|
+
throw new PublishValidationError("CHANGE_NOTE_REQUIRED", "changeNote is required when publishing a version.");
|
|
1713
1750
|
}
|
|
1714
1751
|
return trimmed;
|
|
1715
1752
|
}
|
|
1716
1753
|
__name(assertChangeNote, "assertChangeNote");
|
|
1717
1754
|
function assertDraftPublishable(draft, draftId) {
|
|
1718
1755
|
if (!draft) {
|
|
1719
|
-
throw new PublishValidationError("NOT_FOUND", `PlanVersion ${draftId}
|
|
1756
|
+
throw new PublishValidationError("NOT_FOUND", `PlanVersion ${draftId} not found`);
|
|
1720
1757
|
}
|
|
1721
1758
|
if (draft.publishedAt !== null) {
|
|
1722
|
-
throw new PublishValidationError("ALREADY_PUBLISHED", `PlanVersion ${draftId}
|
|
1759
|
+
throw new PublishValidationError("ALREADY_PUBLISHED", `PlanVersion ${draftId} has already been published.`);
|
|
1723
1760
|
}
|
|
1724
1761
|
if (!draft.baseVersionId) {
|
|
1725
|
-
throw new PublishValidationError("NO_BASE_VERSION", `Draft ${draftId}
|
|
1762
|
+
throw new PublishValidationError("NO_BASE_VERSION", `Draft ${draftId} has no baseVersion \u2014 probably a manually created v1 that may only be published via a migration.`);
|
|
1726
1763
|
}
|
|
1727
1764
|
}
|
|
1728
1765
|
__name(assertDraftPublishable, "assertDraftPublishable");
|
|
1729
1766
|
function assertBaseVersionFresh(base, baseId, planContext) {
|
|
1730
1767
|
if (!base) {
|
|
1731
|
-
throw new PublishValidationError("BASE_NOT_FOUND", `baseVersion ${baseId}
|
|
1768
|
+
throw new PublishValidationError("BASE_NOT_FOUND", `baseVersion ${baseId} no longer exists.`);
|
|
1732
1769
|
}
|
|
1733
1770
|
if (base.supersededAt !== null) {
|
|
1734
|
-
throw new PublishValidationError("BASE_SUPERSEDED", `
|
|
1771
|
+
throw new PublishValidationError("BASE_SUPERSEDED", `The draft is based on a superseded version (${planContext}). Rebase the draft onto the current live version, or create a new one.`);
|
|
1735
1772
|
}
|
|
1736
1773
|
}
|
|
1737
1774
|
__name(assertBaseVersionFresh, "assertBaseVersionFresh");
|
|
1738
1775
|
function assertOptimisticLockHeld(updateCount, draftId) {
|
|
1739
1776
|
if (updateCount !== 1) {
|
|
1740
|
-
throw new PublishValidationError("OPTIMISTIC_LOCK_CONFLICT", `Draft ${draftId}
|
|
1777
|
+
throw new PublishValidationError("OPTIMISTIC_LOCK_CONFLICT", `Draft ${draftId} has meanwhile been published by another admin.`);
|
|
1741
1778
|
}
|
|
1742
1779
|
}
|
|
1743
1780
|
__name(assertOptimisticLockHeld, "assertOptimisticLockHeld");
|
|
@@ -1788,10 +1825,11 @@ var FEATURE_GUARD_CONFIG_TOKEN = /* @__PURE__ */ Symbol.for("saas-platform/Featu
|
|
|
1788
1825
|
// src/billing/feature.guard.ts
|
|
1789
1826
|
var import_common18 = require("@nestjs/common");
|
|
1790
1827
|
var import_core2 = require("@nestjs/core");
|
|
1791
|
-
var
|
|
1828
|
+
var import_types6 = require("@saasicat/types");
|
|
1792
1829
|
|
|
1793
1830
|
// src/entitlement/service.ts
|
|
1794
1831
|
var import_common17 = require("@nestjs/common");
|
|
1832
|
+
var import_types5 = require("@saasicat/types");
|
|
1795
1833
|
|
|
1796
1834
|
// src/catalog/tokens.ts
|
|
1797
1835
|
var PLAN_REPOSITORY_TOKEN = /* @__PURE__ */ Symbol.for("saas-platform/PlanRepository");
|
|
@@ -1966,7 +2004,7 @@ __name(findPlan, "findPlan");
|
|
|
1966
2004
|
function getPlanOrThrow(catalog, planId) {
|
|
1967
2005
|
const plan = findPlan(catalog, planId);
|
|
1968
2006
|
if (!plan) {
|
|
1969
|
-
throw new Error(`Plan "${planId}"
|
|
2007
|
+
throw new Error(`Plan "${planId}" not found in the catalog (${catalog.projectKey})`);
|
|
1970
2008
|
}
|
|
1971
2009
|
return plan;
|
|
1972
2010
|
}
|
|
@@ -2236,7 +2274,7 @@ function assertNoReplacesCycle(index) {
|
|
|
2236
2274
|
...path.slice(cycleStart),
|
|
2237
2275
|
key
|
|
2238
2276
|
].join(" \u2192 ");
|
|
2239
|
-
throw new Error(`replaces
|
|
2277
|
+
throw new Error(`replaces cycle in the discovery metadata: ${cycle}. Feature replacements must be acyclic \u2014 fix the replaces declarations in code (#39).`);
|
|
2240
2278
|
}
|
|
2241
2279
|
state.set(key, IN_PROGRESS);
|
|
2242
2280
|
for (const successor of index.get(key) ?? []) {
|
|
@@ -2262,7 +2300,7 @@ var LimitExceededError = class extends Error {
|
|
|
2262
2300
|
used;
|
|
2263
2301
|
code = LIMIT_EXCEEDED;
|
|
2264
2302
|
constructor(dimension, max, used) {
|
|
2265
|
-
super(`Limit
|
|
2303
|
+
super(`Limit for ${dimension} reached: ${used}/${max}.`), this.dimension = dimension, this.max = max, this.used = used;
|
|
2266
2304
|
this.name = "LimitExceededError";
|
|
2267
2305
|
}
|
|
2268
2306
|
};
|
|
@@ -2357,7 +2395,13 @@ var EntitlementService = class {
|
|
|
2357
2395
|
if (cached) return cached;
|
|
2358
2396
|
const sub = await this.subscriptions.findByTenantId(tenantId);
|
|
2359
2397
|
if (!sub) {
|
|
2360
|
-
throw new import_common17.NotFoundException(
|
|
2398
|
+
throw new import_common17.NotFoundException({
|
|
2399
|
+
code: import_types5.BILLING_ERROR_CODES.SUBSCRIPTION_NOT_FOUND,
|
|
2400
|
+
message: `No subscription for tenant ${tenantId}`,
|
|
2401
|
+
params: {
|
|
2402
|
+
tenantId
|
|
2403
|
+
}
|
|
2404
|
+
});
|
|
2361
2405
|
}
|
|
2362
2406
|
const limits = await this.deriveLimits(sub, now);
|
|
2363
2407
|
this.writeCache(tenantId, limits, now.getTime());
|
|
@@ -2430,7 +2474,7 @@ var EntitlementService = class {
|
|
|
2430
2474
|
return Promise.all(active.map(async (booking) => {
|
|
2431
2475
|
const bv = await this.bundles.findVersionById(booking.bundleVersionId, tx);
|
|
2432
2476
|
if (!bv) {
|
|
2433
|
-
throw new Error(`BundleVersion '${booking.bundleVersionId}'
|
|
2477
|
+
throw new Error(`BundleVersion '${booking.bundleVersionId}' from an active subscription bundle not found`);
|
|
2434
2478
|
}
|
|
2435
2479
|
return {
|
|
2436
2480
|
bundleKey: bv.bundleKey,
|
|
@@ -2452,12 +2496,18 @@ var EntitlementService = class {
|
|
|
2452
2496
|
return this.tx.run(async (tx) => {
|
|
2453
2497
|
const sub = await this.subscriptions.findByTenantIdLocked(input.tenantId, tx);
|
|
2454
2498
|
if (!sub) {
|
|
2455
|
-
throw new import_common17.NotFoundException(
|
|
2499
|
+
throw new import_common17.NotFoundException({
|
|
2500
|
+
code: import_types5.BILLING_ERROR_CODES.SUBSCRIPTION_NOT_FOUND,
|
|
2501
|
+
message: `No subscription for tenant ${input.tenantId}`,
|
|
2502
|
+
params: {
|
|
2503
|
+
tenantId: input.tenantId
|
|
2504
|
+
}
|
|
2505
|
+
});
|
|
2456
2506
|
}
|
|
2457
2507
|
const limits = await this.deriveLimits(sub, now, tx);
|
|
2458
2508
|
const max = limits.quotas[input.dimension];
|
|
2459
2509
|
if (max === void 0) {
|
|
2460
|
-
throw new Error(`Quota-Dimension "${input.dimension}"
|
|
2510
|
+
throw new Error(`Quota-Dimension "${input.dimension}" is not in the plan catalog (${this.catalog.projectKey}).`);
|
|
2461
2511
|
}
|
|
2462
2512
|
const used = await input.currentUsage(tx, input.tenantId);
|
|
2463
2513
|
if (max !== -1 && used + delta > max) {
|
|
@@ -2478,7 +2528,15 @@ var EntitlementService = class {
|
|
|
2478
2528
|
async findActivePlanVersionOrFallback(planId, asOf, tx) {
|
|
2479
2529
|
const v = this.planVersions.findActive ? await this.planVersions.findActive(planId, asOf, tx) : await this.planVersions.findLatestLive(planId, tx);
|
|
2480
2530
|
if (!v) {
|
|
2481
|
-
|
|
2531
|
+
const asOfDate = asOf.toISOString().slice(0, 10);
|
|
2532
|
+
throw new import_common17.NotFoundException({
|
|
2533
|
+
code: import_types5.BILLING_ERROR_CODES.NO_ACTIVE_PLAN_VERSION,
|
|
2534
|
+
message: `No plan version for ${planId} active as of ${asOfDate} \u2014 neither the validFrom window is satisfied nor is a latest-live version available.`,
|
|
2535
|
+
params: {
|
|
2536
|
+
planId,
|
|
2537
|
+
asOf: asOfDate
|
|
2538
|
+
}
|
|
2539
|
+
});
|
|
2482
2540
|
}
|
|
2483
2541
|
return v;
|
|
2484
2542
|
}
|
|
@@ -2581,11 +2639,21 @@ var FeatureGuard = class _FeatureGuard {
|
|
|
2581
2639
|
if (!required || required.length === 0) return true;
|
|
2582
2640
|
const request = context.switchToHttp().getRequest();
|
|
2583
2641
|
const user = request.user;
|
|
2584
|
-
if (!user)
|
|
2642
|
+
if (!user) {
|
|
2643
|
+
throw new import_common18.ForbiddenException({
|
|
2644
|
+
code: import_types6.AUTH_ERROR_CODES.NOT_AUTHENTICATED,
|
|
2645
|
+
message: "Not authenticated"
|
|
2646
|
+
});
|
|
2647
|
+
}
|
|
2585
2648
|
const role = this.config?.userRoleResolver ? this.config.userRoleResolver(user) : user.role ?? user.platformRole;
|
|
2586
2649
|
if (role === "SUPER_ADMIN") return true;
|
|
2587
2650
|
const tenantId = this.config?.tenantIdResolver ? this.config.tenantIdResolver(request) : request.tenantId ?? user.tenantId;
|
|
2588
|
-
if (!tenantId)
|
|
2651
|
+
if (!tenantId) {
|
|
2652
|
+
throw new import_common18.ForbiddenException({
|
|
2653
|
+
code: import_types6.AUTH_ERROR_CODES.NO_TENANT_ASSIGNED,
|
|
2654
|
+
message: "No tenant assigned"
|
|
2655
|
+
});
|
|
2656
|
+
}
|
|
2589
2657
|
const compute = /* @__PURE__ */ __name(() => this.entitlements.computeLimits(tenantId), "compute");
|
|
2590
2658
|
const limits = this.config?.tenantContextRunner ? await this.config.tenantContextRunner(tenantId, compute) : await compute();
|
|
2591
2659
|
const allowed = required.some((f) => limits.features.has(f));
|
|
@@ -2597,19 +2665,30 @@ var FeatureGuard = class _FeatureGuard {
|
|
|
2597
2665
|
/**
|
|
2598
2666
|
* Upsell response (#36): with a registered `UpsellOfferResolver` the 403
|
|
2599
2667
|
* becomes machine-readable (`FeatureNotLicensedBody`), so consumer UIs can
|
|
2600
|
-
* render a purchase offer. Without a resolver the
|
|
2601
|
-
*
|
|
2668
|
+
* render a purchase offer. Without a resolver the body carries the same
|
|
2669
|
+
* `code` but no offers.
|
|
2602
2670
|
*
|
|
2603
2671
|
* Deliberately 403 + `code` field instead of 402 — rationale in
|
|
2604
2672
|
* `@saasicat/types` upsell.types.ts (402 is reserved / inconsistently
|
|
2605
2673
|
* supported; SPA interceptors must not treat the 403 as an auth error, the
|
|
2606
2674
|
* distinction runs through `code`).
|
|
2675
|
+
*
|
|
2676
|
+
* The message must keep starting with the word `Feature` — consumers
|
|
2677
|
+
* classify the response by that prefix.
|
|
2607
2678
|
*/
|
|
2608
2679
|
async buildNotLicensedException(required, tenantId) {
|
|
2609
|
-
const message = `Feature ${required.join(" / ")}
|
|
2610
|
-
if (!this.upsellResolver)
|
|
2680
|
+
const message = `Feature ${required.join(" / ")} is not included in the current plan.`;
|
|
2681
|
+
if (!this.upsellResolver) {
|
|
2682
|
+
return new import_common18.ForbiddenException({
|
|
2683
|
+
code: import_types6.FEATURE_NOT_LICENSED,
|
|
2684
|
+
message,
|
|
2685
|
+
params: {
|
|
2686
|
+
featureKeys: required
|
|
2687
|
+
}
|
|
2688
|
+
});
|
|
2689
|
+
}
|
|
2611
2690
|
const body = {
|
|
2612
|
-
code:
|
|
2691
|
+
code: import_types6.FEATURE_NOT_LICENSED,
|
|
2613
2692
|
featureKey: required[0],
|
|
2614
2693
|
featureKeys: required,
|
|
2615
2694
|
offers: await this.resolveOffersSafe(required, tenantId),
|
|
@@ -2625,7 +2704,7 @@ var FeatureGuard = class _FeatureGuard {
|
|
|
2625
2704
|
try {
|
|
2626
2705
|
return await this.upsellResolver.resolveOffers(required, tenantId);
|
|
2627
2706
|
} catch (error) {
|
|
2628
|
-
this.logger.warn(`UpsellOfferResolver
|
|
2707
|
+
this.logger.warn(`UpsellOfferResolver failed for [${required.join(", ")}]: ${String(error)}`);
|
|
2629
2708
|
return [];
|
|
2630
2709
|
}
|
|
2631
2710
|
}
|
|
@@ -2649,7 +2728,7 @@ FeatureGuard = _ts_decorate16([
|
|
|
2649
2728
|
|
|
2650
2729
|
// src/billing/catalog-bundle-upsell-resolver.ts
|
|
2651
2730
|
var import_common19 = require("@nestjs/common");
|
|
2652
|
-
var
|
|
2731
|
+
var import_types7 = require("@saasicat/types");
|
|
2653
2732
|
|
|
2654
2733
|
// src/billing/public-catalog.tokens.ts
|
|
2655
2734
|
var PUBLIC_CATALOG_PROJECT_KEY_TOKEN = /* @__PURE__ */ Symbol.for("saas-platform/PublicCatalogProjectKey");
|
|
@@ -2719,7 +2798,7 @@ var CatalogBundleUpsellResolver = class {
|
|
|
2719
2798
|
const rows = await this.catalogEntryRepo.listFeatures({
|
|
2720
2799
|
projectKey: this.projectKey
|
|
2721
2800
|
});
|
|
2722
|
-
return (0,
|
|
2801
|
+
return (0, import_types7.collectUnsatisfiedRequires)(featureKeys, (0, import_types7.buildFeatureRequiresIndex)(rows));
|
|
2723
2802
|
}
|
|
2724
2803
|
/** Only published-and-live AND marketed — non-marketed bundles are not an offer. */
|
|
2725
2804
|
async listLiveMarketedBundles() {
|
|
@@ -2778,6 +2857,7 @@ __name(compareRankedOffers, "compareRankedOffers");
|
|
|
2778
2857
|
// src/billing/limit-exceeded.filter.ts
|
|
2779
2858
|
var import_common20 = require("@nestjs/common");
|
|
2780
2859
|
var import_core3 = require("@nestjs/core");
|
|
2860
|
+
var import_types8 = require("@saasicat/types");
|
|
2781
2861
|
function _ts_decorate18(decorators, target, key, desc) {
|
|
2782
2862
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
2783
2863
|
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
@@ -2798,11 +2878,14 @@ var LimitExceededFilter = class _LimitExceededFilter extends import_core3.BaseEx
|
|
|
2798
2878
|
const ctx = host.switchToHttp();
|
|
2799
2879
|
const response = ctx.getResponse();
|
|
2800
2880
|
const request = ctx.getRequest();
|
|
2801
|
-
this.logger.warn(`Limit ${exception.dimension}
|
|
2881
|
+
this.logger.warn(`Limit ${exception.dimension} exceeded: ${exception.used}/${exception.max} on ${request.method ?? "?"} ${request.url ?? ""}`);
|
|
2802
2882
|
response.status(import_common20.HttpStatus.PAYMENT_REQUIRED).send({
|
|
2803
2883
|
statusCode: import_common20.HttpStatus.PAYMENT_REQUIRED,
|
|
2804
2884
|
error: "PaymentRequired",
|
|
2805
|
-
|
|
2885
|
+
code: import_types8.BILLING_ERROR_CODES.LIMIT_EXCEEDED,
|
|
2886
|
+
// Deprecated: superseded by `code`, kept until consumers have
|
|
2887
|
+
// migrated. Same value, only under the older field name.
|
|
2888
|
+
reason: import_types8.BILLING_ERROR_CODES.LIMIT_EXCEEDED,
|
|
2806
2889
|
dimension: exception.dimension,
|
|
2807
2890
|
used: exception.used,
|
|
2808
2891
|
max: exception.max,
|
|
@@ -2828,8 +2911,8 @@ var PlanCatalogValidationError = class extends Error {
|
|
|
2828
2911
|
source;
|
|
2829
2912
|
errors;
|
|
2830
2913
|
constructor(source, errors) {
|
|
2831
|
-
const messages = errors.map((e) => `${e.instancePath || "/"}: ${e.message ?? "
|
|
2832
|
-
super(`Plan
|
|
2914
|
+
const messages = errors.map((e) => `${e.instancePath || "/"}: ${e.message ?? "unknown"}`).join("\n ");
|
|
2915
|
+
super(`Plan catalog validation failed for ${source}:
|
|
2833
2916
|
${messages}`), this.source = source, this.errors = errors;
|
|
2834
2917
|
this.name = "PlanCatalogValidationError";
|
|
2835
2918
|
}
|
|
@@ -2846,7 +2929,7 @@ __name(loadPlanCatalogFromFile, "loadPlanCatalogFromFile");
|
|
|
2846
2929
|
function loadPlanCatalogFromString(yamlContent, opts) {
|
|
2847
2930
|
const parsed = import_js_yaml.default.load(yamlContent);
|
|
2848
2931
|
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
2849
|
-
throw new Error(`YAML
|
|
2932
|
+
throw new Error(`The YAML content of ${opts.source} is not an object`);
|
|
2850
2933
|
}
|
|
2851
2934
|
const ajv = new import__.default({
|
|
2852
2935
|
strict: false,
|
|
@@ -2870,19 +2953,19 @@ function validateConsistency(catalog, source) {
|
|
|
2870
2953
|
const planIds = /* @__PURE__ */ new Set();
|
|
2871
2954
|
for (const plan of catalog.plans ?? []) {
|
|
2872
2955
|
if (planIds.has(plan.id)) {
|
|
2873
|
-
errors.push(`plans[].id:
|
|
2956
|
+
errors.push(`plans[].id: duplicate plan id "${plan.id}"`);
|
|
2874
2957
|
}
|
|
2875
2958
|
planIds.add(plan.id);
|
|
2876
2959
|
if (catalog.features) {
|
|
2877
2960
|
for (const fk of plan.features) {
|
|
2878
2961
|
if (!declaredFeatureKeys.has(fk)) {
|
|
2879
|
-
errors.push(`plans[id=${plan.id}].features:
|
|
2962
|
+
errors.push(`plans[id=${plan.id}].features: Unknown featureKey "${fk}" \u2014 not declared in catalog.features`);
|
|
2880
2963
|
}
|
|
2881
2964
|
}
|
|
2882
2965
|
}
|
|
2883
2966
|
}
|
|
2884
2967
|
if (errors.length > 0) {
|
|
2885
|
-
const err = new Error(`Plan
|
|
2968
|
+
const err = new Error(`Plan catalog consistency error in ${source}:
|
|
2886
2969
|
${errors.join("\n ")}`);
|
|
2887
2970
|
err.name = "PlanCatalogConsistencyError";
|
|
2888
2971
|
throw err;
|
|
@@ -2965,7 +3048,7 @@ var PlanCatalogImporterService = class {
|
|
|
2965
3048
|
if (planResult.created) report.plansCreated++;
|
|
2966
3049
|
else report.plansSkipped++;
|
|
2967
3050
|
if (plan.monthlyNet === null || plan.monthlyNet === void 0) {
|
|
2968
|
-
report.warnings.push(`Plan '${plan.id}'
|
|
3051
|
+
report.warnings.push(`Plan '${plan.id}' has no monthlyNet \u2014 skipped (on-request plans need a manual plan version).`);
|
|
2969
3052
|
continue;
|
|
2970
3053
|
}
|
|
2971
3054
|
const versionResult = await this.sink.upsertPlanVersion({
|
|
@@ -3114,7 +3197,7 @@ var FEATURE_UI_REGISTRY_TOKEN2 = /* @__PURE__ */ Symbol.for("saas-platform/Featu
|
|
|
3114
3197
|
|
|
3115
3198
|
// src/billing/public-catalog.controller.ts
|
|
3116
3199
|
var import_common24 = require("@nestjs/common");
|
|
3117
|
-
var
|
|
3200
|
+
var import_types9 = require("@saasicat/types");
|
|
3118
3201
|
function _ts_decorate22(decorators, target, key, desc) {
|
|
3119
3202
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3120
3203
|
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
@@ -3234,7 +3317,7 @@ var PublicCatalogController = class {
|
|
|
3234
3317
|
const rows = await this.catalogEntryRepo.listFeatures({
|
|
3235
3318
|
projectKey: this.projectKey
|
|
3236
3319
|
});
|
|
3237
|
-
return (0,
|
|
3320
|
+
return (0, import_types9.buildFeatureRequiresIndex)(rows);
|
|
3238
3321
|
} catch {
|
|
3239
3322
|
return /* @__PURE__ */ new Map();
|
|
3240
3323
|
}
|
|
@@ -3274,7 +3357,7 @@ var PublicCatalogController = class {
|
|
|
3274
3357
|
quotas: live.quotas,
|
|
3275
3358
|
monthlyNet: live.monthlyNet,
|
|
3276
3359
|
yearlyNet: live.yearlyNet,
|
|
3277
|
-
requiresFeatures: (0,
|
|
3360
|
+
requiresFeatures: (0, import_types9.collectUnsatisfiedRequires)(features, requiresIndex),
|
|
3278
3361
|
marketing
|
|
3279
3362
|
};
|
|
3280
3363
|
}
|
|
@@ -3398,6 +3481,7 @@ var PENDING_PLAN_QUERY_PORT_TOKEN = /* @__PURE__ */ Symbol.for("saas-platform/Pe
|
|
|
3398
3481
|
|
|
3399
3482
|
// src/billing/composed-tenant-auth.guard.ts
|
|
3400
3483
|
var import_common26 = require("@nestjs/common");
|
|
3484
|
+
var import_types10 = require("@saasicat/types");
|
|
3401
3485
|
function _ts_decorate24(decorators, target, key, desc) {
|
|
3402
3486
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3403
3487
|
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
@@ -3425,7 +3509,10 @@ var ComposedTenantAuthGuard = class {
|
|
|
3425
3509
|
}
|
|
3426
3510
|
async canActivate(context) {
|
|
3427
3511
|
if (!this.guards || this.guards.length === 0) {
|
|
3428
|
-
throw new import_common26.ForbiddenException(
|
|
3512
|
+
throw new import_common26.ForbiddenException({
|
|
3513
|
+
code: import_types10.AUTH_ERROR_CODES.AUTH_GUARDS_NOT_CONFIGURED,
|
|
3514
|
+
message: "TenantBillingModule.forRoot.authGuards is not configured."
|
|
3515
|
+
});
|
|
3429
3516
|
}
|
|
3430
3517
|
for (const guard of this.guards) {
|
|
3431
3518
|
const result = await Promise.resolve(guard.canActivate(context));
|
|
@@ -3446,6 +3533,7 @@ ComposedTenantAuthGuard = _ts_decorate24([
|
|
|
3446
3533
|
|
|
3447
3534
|
// src/billing/tenant-admin.guard.ts
|
|
3448
3535
|
var import_common27 = require("@nestjs/common");
|
|
3536
|
+
var import_types11 = require("@saasicat/types");
|
|
3449
3537
|
function _ts_decorate25(decorators, target, key, desc) {
|
|
3450
3538
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3451
3539
|
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
@@ -3464,11 +3552,17 @@ var TenantAdminGuard = class {
|
|
|
3464
3552
|
canActivate(context) {
|
|
3465
3553
|
const request = context.switchToHttp().getRequest();
|
|
3466
3554
|
if (!request.user) {
|
|
3467
|
-
throw new import_common27.ForbiddenException(
|
|
3555
|
+
throw new import_common27.ForbiddenException({
|
|
3556
|
+
code: import_types11.AUTH_ERROR_CODES.NOT_AUTHENTICATED,
|
|
3557
|
+
message: "Not authenticated"
|
|
3558
|
+
});
|
|
3468
3559
|
}
|
|
3469
3560
|
const role = request.user.platformRole ?? request.user.role;
|
|
3470
3561
|
if (!role || !ADMIN_ROLES.has(role)) {
|
|
3471
|
-
throw new import_common27.ForbiddenException(
|
|
3562
|
+
throw new import_common27.ForbiddenException({
|
|
3563
|
+
code: import_types11.AUTH_ERROR_CODES.TENANT_ADMIN_REQUIRED,
|
|
3564
|
+
message: "This action requires the TENANT_ADMIN role."
|
|
3565
|
+
});
|
|
3472
3566
|
}
|
|
3473
3567
|
return true;
|
|
3474
3568
|
}
|
|
@@ -3506,6 +3600,7 @@ __name(round22, "round2");
|
|
|
3506
3600
|
|
|
3507
3601
|
// src/billing/plan-change-preview.service.ts
|
|
3508
3602
|
var import_common29 = require("@nestjs/common");
|
|
3603
|
+
var import_types12 = require("@saasicat/types");
|
|
3509
3604
|
|
|
3510
3605
|
// src/entitlement/index.ts
|
|
3511
3606
|
var entitlement_exports = {};
|
|
@@ -3634,11 +3729,24 @@ var PlanChangePreviewService = class {
|
|
|
3634
3729
|
async preview(tenantId, targetPlan, targetCycle, now = /* @__PURE__ */ new Date()) {
|
|
3635
3730
|
const sub = await this.subscriptions.findForTenant(tenantId);
|
|
3636
3731
|
if (!sub) {
|
|
3637
|
-
throw new import_common29.NotFoundException(
|
|
3732
|
+
throw new import_common29.NotFoundException({
|
|
3733
|
+
code: import_types12.BILLING_ERROR_CODES.SUBSCRIPTION_NOT_FOUND,
|
|
3734
|
+
message: `No subscription for tenant ${tenantId}`,
|
|
3735
|
+
params: {
|
|
3736
|
+
tenantId
|
|
3737
|
+
}
|
|
3738
|
+
});
|
|
3638
3739
|
}
|
|
3639
3740
|
const targetPlanDef = findPlan(this.catalog, targetPlan);
|
|
3640
3741
|
if (!targetPlanDef) {
|
|
3641
|
-
throw new import_common29.NotFoundException(
|
|
3742
|
+
throw new import_common29.NotFoundException({
|
|
3743
|
+
code: import_types12.BILLING_ERROR_CODES.PLAN_NOT_IN_CATALOG,
|
|
3744
|
+
message: `Plan "${targetPlan}" is not in the catalog (${this.catalog.projectKey})`,
|
|
3745
|
+
params: {
|
|
3746
|
+
planKey: targetPlan,
|
|
3747
|
+
projectKey: this.catalog.projectKey
|
|
3748
|
+
}
|
|
3749
|
+
});
|
|
3642
3750
|
}
|
|
3643
3751
|
const ctx = {
|
|
3644
3752
|
currentPeriodStart: sub.currentPeriodStart,
|
|
@@ -3703,13 +3811,13 @@ var PlanChangePreviewService = class {
|
|
|
3703
3811
|
if (blockedTargets.includes(targetPlan)) {
|
|
3704
3812
|
blockers.push({
|
|
3705
3813
|
code: `${targetPlan}_NOT_SELF_SERVICE`,
|
|
3706
|
-
message: `${targetSnap.name}
|
|
3814
|
+
message: `${targetSnap.name} is only activated via a special contract. Please contact the contract manager.`
|
|
3707
3815
|
});
|
|
3708
3816
|
}
|
|
3709
3817
|
if (blockedSources.includes(sub.plan)) {
|
|
3710
3818
|
blockers.push({
|
|
3711
3819
|
code: `${sub.plan}_LOCKED`,
|
|
3712
|
-
message: `
|
|
3820
|
+
message: `Active ${currentSnap.name} special contract \u2014 please contact the contract manager to change plans.`
|
|
3713
3821
|
});
|
|
3714
3822
|
}
|
|
3715
3823
|
for (const [key, row] of Object.entries(limitsCheck)) {
|
|
@@ -3717,19 +3825,19 @@ var PlanChangePreviewService = class {
|
|
|
3717
3825
|
const usedDisplay = isFloatQuota(key) ? row.used.toFixed(1) : row.used.toString();
|
|
3718
3826
|
blockers.push({
|
|
3719
3827
|
code: `${key.toUpperCase()}_OVER_TARGET`,
|
|
3720
|
-
message: `
|
|
3828
|
+
message: `Current usage ${usedDisplay} exceeds the target limit ${row.targetMax} (${key}) in the ${targetSnap.name} plan. Please reduce usage first.`
|
|
3721
3829
|
});
|
|
3722
3830
|
}
|
|
3723
3831
|
if (featuresLost.length > 0) {
|
|
3724
3832
|
warnings.push({
|
|
3725
3833
|
code: "FEATURES_LOST",
|
|
3726
|
-
message: `
|
|
3834
|
+
message: `Switching means losing access to ${featuresLost.length} feature${featuresLost.length === 1 ? "" : "s"}. Existing data is retained and never deleted \u2014 upgrading again unlocks it.`
|
|
3727
3835
|
});
|
|
3728
3836
|
}
|
|
3729
3837
|
if (changeType === "NOOP") {
|
|
3730
3838
|
warnings.push({
|
|
3731
3839
|
code: "NO_CHANGE",
|
|
3732
|
-
message: "
|
|
3840
|
+
message: "Target plan and billing cycle already match the current state."
|
|
3733
3841
|
});
|
|
3734
3842
|
}
|
|
3735
3843
|
const projectedTrialEndsAt = this.trialProjection && sub.status === "TRIAL" ? await this.trialProjection.projectTrialEndsAt({
|
|
@@ -3887,13 +3995,13 @@ var PendingPlanMaterializationService = class _PendingPlanMaterializationService
|
|
|
3887
3995
|
this.entitlements.invalidateTenant(change.tenantId);
|
|
3888
3996
|
applied += 1;
|
|
3889
3997
|
} catch (err) {
|
|
3890
|
-
this.logger.error(`Pending
|
|
3998
|
+
this.logger.error(`Pending plan materialisation failed (tenant ${change.tenantId}): ${String(err)}`);
|
|
3891
3999
|
continue;
|
|
3892
4000
|
}
|
|
3893
4001
|
await this.tryFreeze(change.tenantId, change.pendingPlan, cycle, now);
|
|
3894
4002
|
}
|
|
3895
4003
|
if (applied > 0) {
|
|
3896
|
-
this.logger.log(`Pending
|
|
4004
|
+
this.logger.log(`Pending plan materialisation: applied ${applied} scheduled plan change(s).`);
|
|
3897
4005
|
}
|
|
3898
4006
|
return {
|
|
3899
4007
|
applied
|
|
@@ -3904,7 +4012,7 @@ var PendingPlanMaterializationService = class _PendingPlanMaterializationService
|
|
|
3904
4012
|
try {
|
|
3905
4013
|
await this.contractFreeze.freezeOnPlanChange(tenantId, plan, cycle, now);
|
|
3906
4014
|
} catch (err) {
|
|
3907
|
-
this.logger.error(`Contract
|
|
4015
|
+
this.logger.error(`Contract freeze after materialisation failed (tenant ${tenantId}): ${String(err)}`);
|
|
3908
4016
|
}
|
|
3909
4017
|
}
|
|
3910
4018
|
};
|
|
@@ -3978,7 +4086,7 @@ function createDiscountLineItem(input, discountNet) {
|
|
|
3978
4086
|
kind: "discount",
|
|
3979
4087
|
sourceKey: promoCode?.code ?? firstPromotion?.id ?? "price-discount",
|
|
3980
4088
|
sourceVersionId: null,
|
|
3981
|
-
titleSnapshot: promoCode?.label ?? firstPromotion?.label ?? "
|
|
4089
|
+
titleSnapshot: promoCode?.label ?? firstPromotion?.label ?? "Discount",
|
|
3982
4090
|
descriptionSnapshot: promoCode ? `Promo-Code ${promoCode.code}` : null,
|
|
3983
4091
|
quantity: 1,
|
|
3984
4092
|
unit: null,
|
|
@@ -4021,6 +4129,7 @@ function cloneJsonValue(value) {
|
|
|
4021
4129
|
__name(cloneJsonValue, "cloneJsonValue");
|
|
4022
4130
|
|
|
4023
4131
|
// src/subscription-contract/subscription-contract.service.ts
|
|
4132
|
+
var import_types13 = require("@saasicat/types");
|
|
4024
4133
|
function _ts_decorate29(decorators, target, key, desc) {
|
|
4025
4134
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4026
4135
|
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
@@ -4052,7 +4161,13 @@ var SubscriptionContractService = class {
|
|
|
4052
4161
|
async getById(contractId) {
|
|
4053
4162
|
const row = await this.repo.findById(contractId);
|
|
4054
4163
|
if (!row) {
|
|
4055
|
-
throw new import_common31.NotFoundException(
|
|
4164
|
+
throw new import_common31.NotFoundException({
|
|
4165
|
+
code: import_types13.CONTRACT_ERROR_CODES.SUBSCRIPTION_CONTRACT_NOT_FOUND,
|
|
4166
|
+
message: `SubscriptionContract '${contractId}' not found`,
|
|
4167
|
+
params: {
|
|
4168
|
+
contractId
|
|
4169
|
+
}
|
|
4170
|
+
});
|
|
4056
4171
|
}
|
|
4057
4172
|
return row;
|
|
4058
4173
|
}
|
|
@@ -4062,7 +4177,14 @@ var SubscriptionContractService = class {
|
|
|
4062
4177
|
async getActiveInvoiceSnapshotForTenant(tenantId, asOf = /* @__PURE__ */ new Date()) {
|
|
4063
4178
|
const contract = await this.repo.findActiveByTenantId(tenantId, asOf);
|
|
4064
4179
|
if (!contract) {
|
|
4065
|
-
throw new import_common31.NotFoundException(
|
|
4180
|
+
throw new import_common31.NotFoundException({
|
|
4181
|
+
code: import_types13.CONTRACT_ERROR_CODES.NO_ACTIVE_SUBSCRIPTION_CONTRACT,
|
|
4182
|
+
message: `No active subscription contract for tenant ${tenantId}`,
|
|
4183
|
+
params: {
|
|
4184
|
+
tenantId,
|
|
4185
|
+
asOf: asOf.toISOString()
|
|
4186
|
+
}
|
|
4187
|
+
});
|
|
4066
4188
|
}
|
|
4067
4189
|
return subscriptionContractToInvoiceSnapshot(contract);
|
|
4068
4190
|
}
|
|
@@ -4095,7 +4217,14 @@ var SubscriptionContractService = class {
|
|
|
4095
4217
|
}
|
|
4096
4218
|
createDataFromOffer(offer, options) {
|
|
4097
4219
|
if (offer.status !== "consumed") {
|
|
4098
|
-
throw new import_common31.ConflictException(
|
|
4220
|
+
throw new import_common31.ConflictException({
|
|
4221
|
+
code: import_types13.CONTRACT_ERROR_CODES.CHECKOUT_OFFER_NOT_CONSUMED,
|
|
4222
|
+
message: `CheckoutOffer '${offer.id}' must be consumed before the contract is created`,
|
|
4223
|
+
params: {
|
|
4224
|
+
offerId: offer.id,
|
|
4225
|
+
status: offer.status
|
|
4226
|
+
}
|
|
4227
|
+
});
|
|
4099
4228
|
}
|
|
4100
4229
|
const lineItems = this.lineItemsFromOffer(offer);
|
|
4101
4230
|
return {
|
|
@@ -4128,8 +4257,8 @@ var SubscriptionContractService = class {
|
|
|
4128
4257
|
const source = offer.lineItems ?? [];
|
|
4129
4258
|
if (source.length === 0) {
|
|
4130
4259
|
throw new import_common31.UnprocessableEntityException({
|
|
4131
|
-
code:
|
|
4132
|
-
message: "
|
|
4260
|
+
code: import_types13.CONTRACT_ERROR_CODES.CHECKOUT_OFFER_LINE_ITEMS_REQUIRED,
|
|
4261
|
+
message: "A checkout offer can yield only one contract, and only once its line items are frozen."
|
|
4133
4262
|
});
|
|
4134
4263
|
}
|
|
4135
4264
|
return appendImplicitDiscountLineItem({
|
|
@@ -4167,8 +4296,11 @@ var SubscriptionContractService = class {
|
|
|
4167
4296
|
const date = value instanceof Date ? new Date(value) : new Date(value);
|
|
4168
4297
|
if (Number.isNaN(date.getTime())) {
|
|
4169
4298
|
throw new import_common31.UnprocessableEntityException({
|
|
4170
|
-
code:
|
|
4171
|
-
message: `${field}
|
|
4299
|
+
code: import_types13.CONTRACT_ERROR_CODES.SUBSCRIPTION_CONTRACT_INVALID_DATE,
|
|
4300
|
+
message: `${field} must be a valid date.`,
|
|
4301
|
+
params: {
|
|
4302
|
+
field
|
|
4303
|
+
}
|
|
4172
4304
|
});
|
|
4173
4305
|
}
|
|
4174
4306
|
return date;
|
|
@@ -4188,32 +4320,39 @@ var SubscriptionContractService = class {
|
|
|
4188
4320
|
assertCreateData(data) {
|
|
4189
4321
|
if (data.lineItems.length === 0) {
|
|
4190
4322
|
throw new import_common31.UnprocessableEntityException({
|
|
4191
|
-
code:
|
|
4192
|
-
message: "
|
|
4323
|
+
code: import_types13.CONTRACT_ERROR_CODES.SUBSCRIPTION_CONTRACT_LINE_ITEMS_REQUIRED,
|
|
4324
|
+
message: "A subscription contract requires at least one line item."
|
|
4193
4325
|
});
|
|
4194
4326
|
}
|
|
4195
4327
|
if (data.effectiveUntil && data.effectiveUntil <= data.effectiveFrom) {
|
|
4196
4328
|
throw new import_common31.UnprocessableEntityException({
|
|
4197
|
-
code:
|
|
4198
|
-
message: "effectiveUntil
|
|
4329
|
+
code: import_types13.CONTRACT_ERROR_CODES.SUBSCRIPTION_CONTRACT_INVALID_WINDOW,
|
|
4330
|
+
message: "effectiveUntil must be after effectiveFrom."
|
|
4199
4331
|
});
|
|
4200
4332
|
}
|
|
4201
4333
|
const planLineItems = data.lineItems.filter((item) => item.kind === "plan");
|
|
4202
4334
|
if (planLineItems.length !== 1) {
|
|
4203
4335
|
throw new import_common31.UnprocessableEntityException({
|
|
4204
|
-
code:
|
|
4205
|
-
message: "
|
|
4336
|
+
code: import_types13.CONTRACT_ERROR_CODES.SUBSCRIPTION_CONTRACT_PLAN_LINE_ITEM_REQUIRED,
|
|
4337
|
+
message: "A subscription contract requires exactly one plan base item."
|
|
4206
4338
|
});
|
|
4207
4339
|
}
|
|
4208
4340
|
}
|
|
4209
4341
|
assertTerminable(existing, data) {
|
|
4210
4342
|
if (existing.status === "terminated" || existing.status === "superseded") {
|
|
4211
|
-
throw new import_common31.ConflictException(
|
|
4343
|
+
throw new import_common31.ConflictException({
|
|
4344
|
+
code: import_types13.CONTRACT_ERROR_CODES.SUBSCRIPTION_CONTRACT_ALREADY_CLOSED,
|
|
4345
|
+
message: `SubscriptionContract '${existing.id}' is already closed`,
|
|
4346
|
+
params: {
|
|
4347
|
+
contractId: existing.id,
|
|
4348
|
+
status: existing.status
|
|
4349
|
+
}
|
|
4350
|
+
});
|
|
4212
4351
|
}
|
|
4213
4352
|
if (data.effectiveUntil <= existing.effectiveFrom) {
|
|
4214
4353
|
throw new import_common31.UnprocessableEntityException({
|
|
4215
|
-
code:
|
|
4216
|
-
message: "effectiveUntil
|
|
4354
|
+
code: import_types13.CONTRACT_ERROR_CODES.SUBSCRIPTION_CONTRACT_TERMINATION_BEFORE_START,
|
|
4355
|
+
message: "effectiveUntil must be after the effectiveFrom of the contract."
|
|
4217
4356
|
});
|
|
4218
4357
|
}
|
|
4219
4358
|
}
|
|
@@ -4505,14 +4644,14 @@ var PreviewPlanChangeDto = class {
|
|
|
4505
4644
|
_ts_decorate32([
|
|
4506
4645
|
(0, import_class_validator3.IsString)(),
|
|
4507
4646
|
(0, import_class_validator3.Matches)(PLAN_OR_CYCLE_PATTERN, {
|
|
4508
|
-
message: "plan
|
|
4647
|
+
message: "plan must be SCREAMING_SNAKE_CASE"
|
|
4509
4648
|
}),
|
|
4510
4649
|
_ts_metadata22("design:type", String)
|
|
4511
4650
|
], PreviewPlanChangeDto.prototype, "plan", void 0);
|
|
4512
4651
|
_ts_decorate32([
|
|
4513
4652
|
(0, import_class_validator3.IsString)(),
|
|
4514
4653
|
(0, import_class_validator3.Matches)(PLAN_OR_CYCLE_PATTERN, {
|
|
4515
|
-
message: "billingCycle
|
|
4654
|
+
message: "billingCycle must be SCREAMING_SNAKE_CASE (e.g. MONTHLY, YEARLY)"
|
|
4516
4655
|
}),
|
|
4517
4656
|
_ts_metadata22("design:type", String)
|
|
4518
4657
|
], PreviewPlanChangeDto.prototype, "billingCycle", void 0);
|
|
@@ -4576,14 +4715,14 @@ var CompleteOnboardingSubscriptionDto = class {
|
|
|
4576
4715
|
_ts_decorate33([
|
|
4577
4716
|
(0, import_class_validator4.IsString)(),
|
|
4578
4717
|
(0, import_class_validator4.Matches)(PLAN_OR_CYCLE_PATTERN2, {
|
|
4579
|
-
message: "plan
|
|
4718
|
+
message: "plan must be SCREAMING_SNAKE_CASE"
|
|
4580
4719
|
}),
|
|
4581
4720
|
_ts_metadata23("design:type", String)
|
|
4582
4721
|
], CompleteOnboardingSubscriptionDto.prototype, "plan", void 0);
|
|
4583
4722
|
_ts_decorate33([
|
|
4584
4723
|
(0, import_class_validator4.IsString)(),
|
|
4585
4724
|
(0, import_class_validator4.Matches)(PLAN_OR_CYCLE_PATTERN2, {
|
|
4586
|
-
message: "billingCycle
|
|
4725
|
+
message: "billingCycle must be SCREAMING_SNAKE_CASE (e.g. MONTHLY, YEARLY)"
|
|
4587
4726
|
}),
|
|
4588
4727
|
_ts_metadata23("design:type", String)
|
|
4589
4728
|
], CompleteOnboardingSubscriptionDto.prototype, "billingCycle", void 0);
|
|
@@ -4591,7 +4730,7 @@ _ts_decorate33([
|
|
|
4591
4730
|
(0, import_class_validator4.IsOptional)(),
|
|
4592
4731
|
(0, import_class_validator4.IsString)(),
|
|
4593
4732
|
(0, import_class_validator4.Matches)(PROMO_CODE_PATTERN, {
|
|
4594
|
-
message: 'promoCode
|
|
4733
|
+
message: 'promoCode must consist of A\u2013Z, 0\u20139, "-", "_" (4\u201332 characters)'
|
|
4595
4734
|
}),
|
|
4596
4735
|
_ts_metadata23("design:type", String)
|
|
4597
4736
|
], CompleteOnboardingSubscriptionDto.prototype, "promoCode", void 0);
|
|
@@ -4607,9 +4746,11 @@ _ts_decorate33([
|
|
|
4607
4746
|
|
|
4608
4747
|
// src/billing/tenant-billing.controller.ts
|
|
4609
4748
|
var import_common36 = require("@nestjs/common");
|
|
4749
|
+
var import_types16 = require("@saasicat/types");
|
|
4610
4750
|
|
|
4611
4751
|
// src/promo/service.ts
|
|
4612
4752
|
var import_common34 = require("@nestjs/common");
|
|
4753
|
+
var import_types14 = require("@saasicat/types");
|
|
4613
4754
|
|
|
4614
4755
|
// src/promo/tokens.ts
|
|
4615
4756
|
var PROMO_CODE_REPOSITORY_TOKEN = /* @__PURE__ */ Symbol.for("saas-platform/PromoCodeRepository");
|
|
@@ -4663,15 +4804,15 @@ function buildLabel(code, cycle) {
|
|
|
4663
4804
|
maximumFractionDigits: 2
|
|
4664
4805
|
})} \u20AC`;
|
|
4665
4806
|
if (code.durationType === "ONCE") {
|
|
4666
|
-
return `${valueStr}
|
|
4807
|
+
return `${valueStr} once`;
|
|
4667
4808
|
}
|
|
4668
4809
|
if (code.durationType === "MONTHS") {
|
|
4669
4810
|
const m = code.durationValue ?? 0;
|
|
4670
|
-
return m === 1 ? `${valueStr}
|
|
4811
|
+
return m === 1 ? `${valueStr} for the first month` : `${valueStr} for ${m} months`;
|
|
4671
4812
|
}
|
|
4672
4813
|
const n = code.durationValue ?? 0;
|
|
4673
|
-
if (n === 1) return cycle === "YEARLY" ? `${valueStr}
|
|
4674
|
-
return cycle === "YEARLY" ? `${valueStr}
|
|
4814
|
+
if (n === 1) return cycle === "YEARLY" ? `${valueStr} for the first year` : `${valueStr} for the first month`;
|
|
4815
|
+
return cycle === "YEARLY" ? `${valueStr} for the first ${n} years` : `${valueStr} for the first ${n} months`;
|
|
4675
4816
|
}
|
|
4676
4817
|
__name(buildLabel, "buildLabel");
|
|
4677
4818
|
|
|
@@ -4693,7 +4834,23 @@ function _ts_param22(paramIndex, decorator) {
|
|
|
4693
4834
|
};
|
|
4694
4835
|
}
|
|
4695
4836
|
__name(_ts_param22, "_ts_param");
|
|
4696
|
-
var
|
|
4837
|
+
var CODE_MIN_LENGTH = 4;
|
|
4838
|
+
var CODE_MAX_LENGTH = 32;
|
|
4839
|
+
var CODE_PATTERN = new RegExp(`^[A-Z0-9_-]{${CODE_MIN_LENGTH},${CODE_MAX_LENGTH}}$`);
|
|
4840
|
+
var PERCENT_MIN_EXCLUSIVE = 0;
|
|
4841
|
+
var PERCENT_MAX = 100;
|
|
4842
|
+
var DURATION_MIN = 1;
|
|
4843
|
+
var DURATION_MAX = 24;
|
|
4844
|
+
function notRedeemable(reason) {
|
|
4845
|
+
return new import_common34.BadRequestException({
|
|
4846
|
+
code: import_types14.PROMO_ERROR_CODES.PROMO_CODE_NOT_REDEEMABLE,
|
|
4847
|
+
message: `Code cannot be redeemed: ${reason}`,
|
|
4848
|
+
params: {
|
|
4849
|
+
reason
|
|
4850
|
+
}
|
|
4851
|
+
});
|
|
4852
|
+
}
|
|
4853
|
+
__name(notRedeemable, "notRedeemable");
|
|
4697
4854
|
var PromoCodesService = class {
|
|
4698
4855
|
static {
|
|
4699
4856
|
__name(this, "PromoCodesService");
|
|
@@ -4722,46 +4879,118 @@ var PromoCodesService = class {
|
|
|
4722
4879
|
async create(input) {
|
|
4723
4880
|
const code = input.code.trim().toUpperCase();
|
|
4724
4881
|
if (!CODE_PATTERN.test(code)) {
|
|
4725
|
-
throw new import_common34.BadRequestException(
|
|
4882
|
+
throw new import_common34.BadRequestException({
|
|
4883
|
+
code: import_types14.PROMO_ERROR_CODES.PROMO_CODE_FORMAT_INVALID,
|
|
4884
|
+
message: 'The code may only contain upper-case letters, digits, "-" and "_" (4\u201332 characters).',
|
|
4885
|
+
params: {
|
|
4886
|
+
promoCode: code,
|
|
4887
|
+
minLength: CODE_MIN_LENGTH,
|
|
4888
|
+
maxLength: CODE_MAX_LENGTH
|
|
4889
|
+
}
|
|
4890
|
+
});
|
|
4726
4891
|
}
|
|
4727
4892
|
if (input.valueType === "PERCENT") {
|
|
4728
|
-
if (!(input.value >
|
|
4729
|
-
throw new import_common34.BadRequestException(
|
|
4893
|
+
if (!(input.value > PERCENT_MIN_EXCLUSIVE && input.value <= PERCENT_MAX)) {
|
|
4894
|
+
throw new import_common34.BadRequestException({
|
|
4895
|
+
code: import_types14.PROMO_ERROR_CODES.PROMO_PERCENT_OUT_OF_RANGE,
|
|
4896
|
+
message: "The percentage must be between 0 and 100.",
|
|
4897
|
+
params: {
|
|
4898
|
+
value: input.value,
|
|
4899
|
+
min: PERCENT_MIN_EXCLUSIVE,
|
|
4900
|
+
max: PERCENT_MAX
|
|
4901
|
+
}
|
|
4902
|
+
});
|
|
4730
4903
|
}
|
|
4731
4904
|
} else if (!(input.value > 0)) {
|
|
4732
|
-
throw new import_common34.BadRequestException(
|
|
4905
|
+
throw new import_common34.BadRequestException({
|
|
4906
|
+
code: import_types14.PROMO_ERROR_CODES.PROMO_AMOUNT_NOT_POSITIVE,
|
|
4907
|
+
message: "The amount must be positive.",
|
|
4908
|
+
params: {
|
|
4909
|
+
value: input.value,
|
|
4910
|
+
min: 0
|
|
4911
|
+
}
|
|
4912
|
+
});
|
|
4733
4913
|
}
|
|
4734
4914
|
if (input.durationType === "ONCE") {
|
|
4735
4915
|
if (input.durationValue != null) {
|
|
4736
|
-
throw new import_common34.BadRequestException(
|
|
4916
|
+
throw new import_common34.BadRequestException({
|
|
4917
|
+
code: import_types14.PROMO_ERROR_CODES.PROMO_ONE_OFF_WITH_DURATION,
|
|
4918
|
+
message: "A one-off discount must not set a duration.",
|
|
4919
|
+
params: {
|
|
4920
|
+
durationValue: input.durationValue
|
|
4921
|
+
}
|
|
4922
|
+
});
|
|
4737
4923
|
}
|
|
4738
4924
|
} else {
|
|
4739
4925
|
const v = input.durationValue;
|
|
4740
|
-
if (v == null || v <
|
|
4741
|
-
throw new import_common34.BadRequestException(
|
|
4926
|
+
if (v == null || v < DURATION_MIN || v > DURATION_MAX) {
|
|
4927
|
+
throw new import_common34.BadRequestException({
|
|
4928
|
+
code: import_types14.PROMO_ERROR_CODES.PROMO_DURATION_INVALID,
|
|
4929
|
+
message: "Invalid duration (at most 24 months or billing periods).",
|
|
4930
|
+
params: {
|
|
4931
|
+
durationValue: v ?? null,
|
|
4932
|
+
min: DURATION_MIN,
|
|
4933
|
+
max: DURATION_MAX
|
|
4934
|
+
}
|
|
4935
|
+
});
|
|
4742
4936
|
}
|
|
4743
4937
|
}
|
|
4744
4938
|
if (input.validFrom && input.validUntil && input.validFrom >= input.validUntil) {
|
|
4745
|
-
throw new import_common34.BadRequestException(
|
|
4939
|
+
throw new import_common34.BadRequestException({
|
|
4940
|
+
code: import_types14.PROMO_ERROR_CODES.PROMO_VALIDITY_WINDOW_INVALID,
|
|
4941
|
+
message: "Invalid validity window.",
|
|
4942
|
+
params: {
|
|
4943
|
+
validFrom: input.validFrom.toISOString(),
|
|
4944
|
+
validUntil: input.validUntil.toISOString()
|
|
4945
|
+
}
|
|
4946
|
+
});
|
|
4746
4947
|
}
|
|
4747
4948
|
const plans = input.appliesToPlans ?? [];
|
|
4748
4949
|
const blocked = this.config.nonRedeemablePlans ?? [];
|
|
4749
4950
|
for (const p of plans) {
|
|
4750
4951
|
if (blocked.includes(p)) {
|
|
4751
|
-
throw new import_common34.BadRequestException(
|
|
4952
|
+
throw new import_common34.BadRequestException({
|
|
4953
|
+
code: import_types14.PROMO_ERROR_CODES.PROMO_PLAN_NOT_DISCOUNTABLE,
|
|
4954
|
+
message: `${p}-Plan cannot be discounted.`,
|
|
4955
|
+
params: {
|
|
4956
|
+
plan: p
|
|
4957
|
+
}
|
|
4958
|
+
});
|
|
4752
4959
|
}
|
|
4753
4960
|
}
|
|
4754
4961
|
if (input.minimumPlanAmountGross != null && input.minimumPlanAmountGross <= 0) {
|
|
4755
|
-
throw new import_common34.BadRequestException(
|
|
4962
|
+
throw new import_common34.BadRequestException({
|
|
4963
|
+
code: import_types14.PROMO_ERROR_CODES.PROMO_MIN_AMOUNT_NOT_POSITIVE,
|
|
4964
|
+
message: "The minimum plan gross amount must be positive.",
|
|
4965
|
+
params: {
|
|
4966
|
+
value: input.minimumPlanAmountGross,
|
|
4967
|
+
min: 0
|
|
4968
|
+
}
|
|
4969
|
+
});
|
|
4756
4970
|
}
|
|
4757
4971
|
if (input.valueType === "ABSOLUTE" && !input.allowZeroInvoice) {
|
|
4758
4972
|
const lowestApplicableGross = this.lowestApplicablePlanGross(plans);
|
|
4759
4973
|
if (lowestApplicableGross != null && input.value >= lowestApplicableGross) {
|
|
4760
|
-
throw new import_common34.BadRequestException(
|
|
4974
|
+
throw new import_common34.BadRequestException({
|
|
4975
|
+
code: import_types14.PROMO_ERROR_CODES.PROMO_WOULD_PRODUCE_ZERO_INVOICE,
|
|
4976
|
+
message: "For absolute amounts the discount must stay below the lowest applicable plan price, or allowZeroInvoice must be enabled.",
|
|
4977
|
+
params: {
|
|
4978
|
+
value: input.value,
|
|
4979
|
+
lowestApplicablePlanGross: lowestApplicableGross
|
|
4980
|
+
}
|
|
4981
|
+
});
|
|
4761
4982
|
}
|
|
4762
4983
|
}
|
|
4763
4984
|
const exists = await this.promoRepo.findByCode(code);
|
|
4764
|
-
if (exists)
|
|
4985
|
+
if (exists) {
|
|
4986
|
+
throw new import_common34.BadRequestException({
|
|
4987
|
+
code: import_types14.PROMO_ERROR_CODES.PROMO_CODE_ALREADY_EXISTS,
|
|
4988
|
+
message: "The code already exists.",
|
|
4989
|
+
params: {
|
|
4990
|
+
promoCode: code
|
|
4991
|
+
}
|
|
4992
|
+
});
|
|
4993
|
+
}
|
|
4765
4994
|
return this.promoRepo.create({
|
|
4766
4995
|
...input,
|
|
4767
4996
|
code
|
|
@@ -4769,10 +4998,25 @@ var PromoCodesService = class {
|
|
|
4769
4998
|
}
|
|
4770
4999
|
async update(id, input) {
|
|
4771
5000
|
const existing = await this.promoRepo.findById(id);
|
|
4772
|
-
if (!existing)
|
|
5001
|
+
if (!existing) {
|
|
5002
|
+
throw new import_common34.NotFoundException({
|
|
5003
|
+
code: import_types14.PROMO_ERROR_CODES.PROMO_CODE_NOT_FOUND,
|
|
5004
|
+
message: "Code not found",
|
|
5005
|
+
params: {
|
|
5006
|
+
promoCodeId: id
|
|
5007
|
+
}
|
|
5008
|
+
});
|
|
5009
|
+
}
|
|
4773
5010
|
if (input.maxRedemptions != null && existing.maxRedemptions != null) {
|
|
4774
5011
|
if (input.maxRedemptions < existing.maxRedemptions) {
|
|
4775
|
-
throw new import_common34.BadRequestException(
|
|
5012
|
+
throw new import_common34.BadRequestException({
|
|
5013
|
+
code: import_types14.PROMO_ERROR_CODES.PROMO_MAX_REDEMPTIONS_LOWERED,
|
|
5014
|
+
message: "maxRedemptions cannot be lowered.",
|
|
5015
|
+
params: {
|
|
5016
|
+
current: existing.maxRedemptions,
|
|
5017
|
+
requested: input.maxRedemptions
|
|
5018
|
+
}
|
|
5019
|
+
});
|
|
4776
5020
|
}
|
|
4777
5021
|
}
|
|
4778
5022
|
return this.promoRepo.update(id, input);
|
|
@@ -4780,7 +5024,14 @@ var PromoCodesService = class {
|
|
|
4780
5024
|
async softDelete(id) {
|
|
4781
5025
|
const redemptions = await this.redemptionRepo.countByPromoCode(id);
|
|
4782
5026
|
if (redemptions > 0) {
|
|
4783
|
-
throw new import_common34.BadRequestException(
|
|
5027
|
+
throw new import_common34.BadRequestException({
|
|
5028
|
+
code: import_types14.PROMO_ERROR_CODES.PROMO_CODE_HAS_REDEMPTIONS,
|
|
5029
|
+
message: "The code already has redemptions \u2014 it cannot be soft-deleted. Pause it instead.",
|
|
5030
|
+
params: {
|
|
5031
|
+
promoCodeId: id,
|
|
5032
|
+
redemptions
|
|
5033
|
+
}
|
|
5034
|
+
});
|
|
4784
5035
|
}
|
|
4785
5036
|
await this.promoRepo.softDelete(id);
|
|
4786
5037
|
}
|
|
@@ -4791,7 +5042,15 @@ var PromoCodesService = class {
|
|
|
4791
5042
|
async findOne(id) {
|
|
4792
5043
|
await this.lazyExpire();
|
|
4793
5044
|
const code = await this.promoRepo.findById(id);
|
|
4794
|
-
if (!code || code.deletedAt)
|
|
5045
|
+
if (!code || code.deletedAt) {
|
|
5046
|
+
throw new import_common34.NotFoundException({
|
|
5047
|
+
code: import_types14.PROMO_ERROR_CODES.PROMO_CODE_NOT_FOUND,
|
|
5048
|
+
message: "Code not found",
|
|
5049
|
+
params: {
|
|
5050
|
+
promoCodeId: id
|
|
5051
|
+
}
|
|
5052
|
+
});
|
|
5053
|
+
}
|
|
4795
5054
|
return code;
|
|
4796
5055
|
}
|
|
4797
5056
|
async stats(id) {
|
|
@@ -4919,28 +5178,49 @@ var PromoCodesService = class {
|
|
|
4919
5178
|
const code = input.code.trim().toUpperCase();
|
|
4920
5179
|
const promo = await this.promoRepo.findByCode(code, tx);
|
|
4921
5180
|
if (!promo || promo.deletedAt) {
|
|
4922
|
-
throw new import_common34.BadRequestException(
|
|
5181
|
+
throw new import_common34.BadRequestException({
|
|
5182
|
+
code: import_types14.PROMO_ERROR_CODES.PROMO_CODE_NOT_FOUND,
|
|
5183
|
+
message: "Code not found",
|
|
5184
|
+
params: {
|
|
5185
|
+
promoCode: code
|
|
5186
|
+
}
|
|
5187
|
+
});
|
|
4923
5188
|
}
|
|
4924
5189
|
const sub = await this.subscriptionLookup.findById(input.subscriptionId, tx);
|
|
4925
|
-
if (!sub)
|
|
5190
|
+
if (!sub) {
|
|
5191
|
+
throw new import_common34.NotFoundException({
|
|
5192
|
+
code: import_types14.BILLING_ERROR_CODES.SUBSCRIPTION_NOT_FOUND,
|
|
5193
|
+
message: "Subscription not found",
|
|
5194
|
+
params: {
|
|
5195
|
+
subscriptionId: input.subscriptionId
|
|
5196
|
+
}
|
|
5197
|
+
});
|
|
5198
|
+
}
|
|
4926
5199
|
if (sub.tenantId !== input.tenantId) {
|
|
4927
|
-
throw new import_common34.BadRequestException(
|
|
5200
|
+
throw new import_common34.BadRequestException({
|
|
5201
|
+
code: import_types14.BILLING_ERROR_CODES.SUBSCRIPTION_TENANT_MISMATCH,
|
|
5202
|
+
message: "Subscription does not belong to the tenant",
|
|
5203
|
+
params: {
|
|
5204
|
+
subscriptionId: sub.id,
|
|
5205
|
+
tenantId: input.tenantId
|
|
5206
|
+
}
|
|
5207
|
+
});
|
|
4928
5208
|
}
|
|
4929
5209
|
const reason = this.checkEligibility(promo, {
|
|
4930
5210
|
code,
|
|
4931
5211
|
planId: sub.plan,
|
|
4932
5212
|
billingCycle: sub.billingCycle
|
|
4933
5213
|
});
|
|
4934
|
-
if (reason) throw
|
|
5214
|
+
if (reason) throw notRedeemable(reason);
|
|
4935
5215
|
const firstTimeReason = await this.checkFirstTimeCustomer(promo, input.email, {
|
|
4936
5216
|
requireEmail: true
|
|
4937
5217
|
});
|
|
4938
5218
|
if (firstTimeReason) {
|
|
4939
|
-
throw
|
|
5219
|
+
throw notRedeemable(firstTimeReason);
|
|
4940
5220
|
}
|
|
4941
5221
|
const claimed = await this.promoRepo.claimSlot(promo.id, tx);
|
|
4942
5222
|
if (!claimed) {
|
|
4943
|
-
throw
|
|
5223
|
+
throw notRedeemable("EXHAUSTED");
|
|
4944
5224
|
}
|
|
4945
5225
|
await this.promoRepo.markExhaustedIfFull(promo.id, tx);
|
|
4946
5226
|
const startsAt = sub.startedAt ?? /* @__PURE__ */ new Date();
|
|
@@ -5051,6 +5331,7 @@ PromoCodesService = _ts_decorate34([
|
|
|
5051
5331
|
|
|
5052
5332
|
// src/billing/subscription-bundles.service.ts
|
|
5053
5333
|
var import_common35 = require("@nestjs/common");
|
|
5334
|
+
var import_types15 = require("@saasicat/types");
|
|
5054
5335
|
function _ts_decorate35(decorators, target, key, desc) {
|
|
5055
5336
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
5056
5337
|
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
@@ -5098,38 +5379,61 @@ var SubscriptionBundlesService = class {
|
|
|
5098
5379
|
async addBundleToSubscription(input) {
|
|
5099
5380
|
const bundleVersion = await this.bundles.findVersionById(input.bundleVersionId);
|
|
5100
5381
|
if (!bundleVersion) {
|
|
5101
|
-
throw new import_common35.NotFoundException(
|
|
5382
|
+
throw new import_common35.NotFoundException({
|
|
5383
|
+
code: import_types15.CATALOG_ERROR_CODES.BUNDLE_VERSION_NOT_FOUND,
|
|
5384
|
+
message: `BundleVersion '${input.bundleVersionId}' not found`,
|
|
5385
|
+
params: {
|
|
5386
|
+
bundleVersionId: input.bundleVersionId
|
|
5387
|
+
}
|
|
5388
|
+
});
|
|
5102
5389
|
}
|
|
5103
5390
|
if (bundleVersion.publishedAt === null) {
|
|
5104
5391
|
throw new import_common35.UnprocessableEntityException({
|
|
5105
|
-
code:
|
|
5106
|
-
message: `BundleVersion '${input.bundleVersionId}'
|
|
5392
|
+
code: import_types15.CATALOG_ERROR_CODES.BUNDLE_VERSION_NOT_PUBLISHED,
|
|
5393
|
+
message: `BundleVersion '${input.bundleVersionId}' is not published and cannot be booked.`,
|
|
5394
|
+
params: {
|
|
5395
|
+
bundleVersionId: input.bundleVersionId
|
|
5396
|
+
}
|
|
5107
5397
|
});
|
|
5108
5398
|
}
|
|
5109
5399
|
if (bundleVersion.supersededAt !== null) {
|
|
5110
5400
|
throw new import_common35.UnprocessableEntityException({
|
|
5111
|
-
code:
|
|
5112
|
-
message: `BundleVersion '${input.bundleVersionId}'
|
|
5401
|
+
code: import_types15.CATALOG_ERROR_CODES.BUNDLE_VERSION_SUPERSEDED,
|
|
5402
|
+
message: `BundleVersion '${input.bundleVersionId}' has been superseded by a newer version.`,
|
|
5403
|
+
params: {
|
|
5404
|
+
bundleVersionId: input.bundleVersionId
|
|
5405
|
+
}
|
|
5113
5406
|
});
|
|
5114
5407
|
}
|
|
5115
5408
|
if (this.blockedBundles?.bundleKeys?.includes(bundleVersion.bundleKey)) {
|
|
5116
5409
|
throw new import_common35.UnprocessableEntityException({
|
|
5117
|
-
code:
|
|
5118
|
-
message: `Bundle '${bundleVersion.bundleKey}'
|
|
5410
|
+
code: import_types15.BILLING_ERROR_CODES.BUNDLE_NOT_SELF_SERVICE,
|
|
5411
|
+
message: `Bundle '${bundleVersion.bundleKey}' is only activated via a special contract. Please contact the contract manager.`,
|
|
5412
|
+
params: {
|
|
5413
|
+
bundleKey: bundleVersion.bundleKey
|
|
5414
|
+
}
|
|
5119
5415
|
});
|
|
5120
5416
|
}
|
|
5121
5417
|
const planIds = bundleVersion.compatibility?.planIds ?? [];
|
|
5122
5418
|
if (planIds.length > 0 && !planIds.includes(input.currentPlanKey)) {
|
|
5123
5419
|
throw new import_common35.UnprocessableEntityException({
|
|
5124
|
-
code:
|
|
5125
|
-
message: `BundleVersion '${input.bundleVersionId}'
|
|
5420
|
+
code: import_types15.BILLING_ERROR_CODES.BUNDLE_INCOMPATIBLE_WITH_PLAN,
|
|
5421
|
+
message: `BundleVersion '${input.bundleVersionId}' is not compatible with plan '${input.currentPlanKey}'. Allowed: [${planIds.join(", ")}].`,
|
|
5422
|
+
params: {
|
|
5423
|
+
bundleVersionId: input.bundleVersionId,
|
|
5424
|
+
planKey: input.currentPlanKey,
|
|
5425
|
+
allowedPlanKeys: planIds
|
|
5426
|
+
}
|
|
5126
5427
|
});
|
|
5127
5428
|
}
|
|
5128
5429
|
const active = await this.repo.listActiveBySubscription(input.subscriptionId);
|
|
5129
5430
|
if (active.some((b) => b.bundleVersionId === input.bundleVersionId)) {
|
|
5130
5431
|
throw new import_common35.UnprocessableEntityException({
|
|
5131
|
-
code:
|
|
5132
|
-
message: `Subscription '${input.subscriptionId}'
|
|
5432
|
+
code: import_types15.BILLING_ERROR_CODES.BUNDLE_ALREADY_SUBSCRIBED,
|
|
5433
|
+
message: `Subscription '${input.subscriptionId}' has already actively booked this bundle.`,
|
|
5434
|
+
params: {
|
|
5435
|
+
subscriptionId: input.subscriptionId
|
|
5436
|
+
}
|
|
5133
5437
|
});
|
|
5134
5438
|
}
|
|
5135
5439
|
const startedAt = input.startedAt ?? /* @__PURE__ */ new Date();
|
|
@@ -5145,12 +5449,21 @@ var SubscriptionBundlesService = class {
|
|
|
5145
5449
|
async cancelBundleFromSubscription(input) {
|
|
5146
5450
|
const existing = await this.repo.findById(input.subscriptionBundleId);
|
|
5147
5451
|
if (!existing) {
|
|
5148
|
-
throw new import_common35.NotFoundException(
|
|
5452
|
+
throw new import_common35.NotFoundException({
|
|
5453
|
+
code: import_types15.BILLING_ERROR_CODES.SUBSCRIPTION_BUNDLE_NOT_FOUND,
|
|
5454
|
+
message: `SubscriptionBundle '${input.subscriptionBundleId}' not found`,
|
|
5455
|
+
params: {
|
|
5456
|
+
subscriptionBundleId: input.subscriptionBundleId
|
|
5457
|
+
}
|
|
5458
|
+
});
|
|
5149
5459
|
}
|
|
5150
5460
|
if (existing.canceledAt !== null) {
|
|
5151
5461
|
throw new import_common35.UnprocessableEntityException({
|
|
5152
|
-
code:
|
|
5153
|
-
message: `SubscriptionBundle '${input.subscriptionBundleId}'
|
|
5462
|
+
code: import_types15.BILLING_ERROR_CODES.SUBSCRIPTION_BUNDLE_ALREADY_CANCELED,
|
|
5463
|
+
message: `SubscriptionBundle '${input.subscriptionBundleId}' is already cancelled.`,
|
|
5464
|
+
params: {
|
|
5465
|
+
subscriptionBundleId: input.subscriptionBundleId
|
|
5466
|
+
}
|
|
5154
5467
|
});
|
|
5155
5468
|
}
|
|
5156
5469
|
const canceledAt = input.canceledAt ?? /* @__PURE__ */ new Date();
|
|
@@ -5171,18 +5484,27 @@ var SubscriptionBundlesService = class {
|
|
|
5171
5484
|
async reactivateBundle(subscriptionBundleId) {
|
|
5172
5485
|
const existing = await this.repo.findById(subscriptionBundleId);
|
|
5173
5486
|
if (!existing) {
|
|
5174
|
-
throw new import_common35.NotFoundException(
|
|
5487
|
+
throw new import_common35.NotFoundException({
|
|
5488
|
+
code: import_types15.BILLING_ERROR_CODES.SUBSCRIPTION_BUNDLE_NOT_FOUND,
|
|
5489
|
+
message: `SubscriptionBundle '${subscriptionBundleId}' not found`,
|
|
5490
|
+
params: {
|
|
5491
|
+
subscriptionBundleId
|
|
5492
|
+
}
|
|
5493
|
+
});
|
|
5175
5494
|
}
|
|
5176
5495
|
if (existing.canceledAt === null) {
|
|
5177
5496
|
throw new import_common35.UnprocessableEntityException({
|
|
5178
|
-
code:
|
|
5179
|
-
message: `SubscriptionBundle '${subscriptionBundleId}'
|
|
5497
|
+
code: import_types15.BILLING_ERROR_CODES.SUBSCRIPTION_BUNDLE_NOT_CANCELED,
|
|
5498
|
+
message: `SubscriptionBundle '${subscriptionBundleId}' is not cancelled.`,
|
|
5499
|
+
params: {
|
|
5500
|
+
subscriptionBundleId
|
|
5501
|
+
}
|
|
5180
5502
|
});
|
|
5181
5503
|
}
|
|
5182
5504
|
if (existing.canceledEffectiveAt !== null && existing.canceledEffectiveAt <= /* @__PURE__ */ new Date()) {
|
|
5183
5505
|
throw new import_common35.UnprocessableEntityException({
|
|
5184
|
-
code:
|
|
5185
|
-
message: "
|
|
5506
|
+
code: import_types15.BILLING_ERROR_CODES.SUBSCRIPTION_BUNDLE_CANCELLATION_EFFECTIVE,
|
|
5507
|
+
message: "Cancellation already in effect \u2014 book the bundle again."
|
|
5186
5508
|
});
|
|
5187
5509
|
}
|
|
5188
5510
|
return this.repo.reactivate(subscriptionBundleId);
|
|
@@ -5288,7 +5610,13 @@ var TenantBillingController = class _TenantBillingController {
|
|
|
5288
5610
|
this.usageSnapshot.snapshot(tenantId)
|
|
5289
5611
|
]);
|
|
5290
5612
|
if (!sub) {
|
|
5291
|
-
throw new import_common36.NotFoundException(
|
|
5613
|
+
throw new import_common36.NotFoundException({
|
|
5614
|
+
code: import_types16.BILLING_ERROR_CODES.SUBSCRIPTION_NOT_FOUND,
|
|
5615
|
+
message: `No subscription for tenant ${tenantId}`,
|
|
5616
|
+
params: {
|
|
5617
|
+
tenantId
|
|
5618
|
+
}
|
|
5619
|
+
});
|
|
5292
5620
|
}
|
|
5293
5621
|
const usage = {};
|
|
5294
5622
|
for (const key of /* @__PURE__ */ new Set([
|
|
@@ -5337,14 +5665,29 @@ var TenantBillingController = class _TenantBillingController {
|
|
|
5337
5665
|
const userId = this.requireUserId(req);
|
|
5338
5666
|
const blockedTargets = this.blockedPlans?.asTarget ?? [];
|
|
5339
5667
|
if (blockedTargets.includes(dto.plan)) {
|
|
5340
|
-
throw new import_common36.ForbiddenException(
|
|
5668
|
+
throw new import_common36.ForbiddenException({
|
|
5669
|
+
code: import_types16.BILLING_ERROR_CODES.PLAN_NOT_SELF_SERVICE,
|
|
5670
|
+
message: `${dto.plan} is not activated via self-service.`,
|
|
5671
|
+
params: {
|
|
5672
|
+
planKey: dto.plan
|
|
5673
|
+
}
|
|
5674
|
+
});
|
|
5341
5675
|
}
|
|
5342
5676
|
const sub = await this.subscriptionUsage.findForTenant(tenantId);
|
|
5343
|
-
if (!sub)
|
|
5677
|
+
if (!sub) {
|
|
5678
|
+
throw new import_common36.NotFoundException({
|
|
5679
|
+
code: import_types16.BILLING_ERROR_CODES.SUBSCRIPTION_NOT_FOUND,
|
|
5680
|
+
message: `No subscription for tenant ${tenantId}`,
|
|
5681
|
+
params: {
|
|
5682
|
+
tenantId
|
|
5683
|
+
}
|
|
5684
|
+
});
|
|
5685
|
+
}
|
|
5344
5686
|
const blockers = await this.planPreview.assertChangeAllowed(tenantId, dto.plan, dto.billingCycle);
|
|
5345
5687
|
if (blockers.length > 0) {
|
|
5346
5688
|
throw new import_common36.BadRequestException({
|
|
5347
|
-
|
|
5689
|
+
code: import_types16.BILLING_ERROR_CODES.PLAN_CHANGE_BLOCKED,
|
|
5690
|
+
message: "Plan change is blocked.",
|
|
5348
5691
|
blockers
|
|
5349
5692
|
});
|
|
5350
5693
|
}
|
|
@@ -5414,14 +5757,29 @@ var TenantBillingController = class _TenantBillingController {
|
|
|
5414
5757
|
const warnings = [];
|
|
5415
5758
|
const blockedTargets = this.blockedPlans?.asTarget ?? [];
|
|
5416
5759
|
if (blockedTargets.includes(dto.plan)) {
|
|
5417
|
-
throw new import_common36.ForbiddenException(
|
|
5760
|
+
throw new import_common36.ForbiddenException({
|
|
5761
|
+
code: import_types16.BILLING_ERROR_CODES.PLAN_NOT_SELF_SERVICE,
|
|
5762
|
+
message: `${dto.plan} is not activated via self-service.`,
|
|
5763
|
+
params: {
|
|
5764
|
+
planKey: dto.plan
|
|
5765
|
+
}
|
|
5766
|
+
});
|
|
5418
5767
|
}
|
|
5419
5768
|
const sub = await this.subscriptionUsage.findForTenant(tenantId);
|
|
5420
|
-
if (!sub)
|
|
5769
|
+
if (!sub) {
|
|
5770
|
+
throw new import_common36.NotFoundException({
|
|
5771
|
+
code: import_types16.BILLING_ERROR_CODES.SUBSCRIPTION_NOT_FOUND,
|
|
5772
|
+
message: `No subscription for tenant ${tenantId}`,
|
|
5773
|
+
params: {
|
|
5774
|
+
tenantId
|
|
5775
|
+
}
|
|
5776
|
+
});
|
|
5777
|
+
}
|
|
5421
5778
|
const blockers = await this.planPreview.assertChangeAllowed(tenantId, dto.plan, dto.billingCycle);
|
|
5422
5779
|
if (blockers.length > 0) {
|
|
5423
5780
|
throw new import_common36.BadRequestException({
|
|
5424
|
-
|
|
5781
|
+
code: import_types16.BILLING_ERROR_CODES.PLAN_CHANGE_BLOCKED,
|
|
5782
|
+
message: "Plan change during onboarding is blocked.",
|
|
5425
5783
|
blockers
|
|
5426
5784
|
});
|
|
5427
5785
|
}
|
|
@@ -5455,8 +5813,13 @@ var TenantBillingController = class _TenantBillingController {
|
|
|
5455
5813
|
warnings.push(...this.collectPromoSkipReasons(dto.promoCode, sub));
|
|
5456
5814
|
}
|
|
5457
5815
|
} catch (err) {
|
|
5816
|
+
const reason = err instanceof Error ? err.message : null;
|
|
5458
5817
|
throw new import_common36.BadRequestException({
|
|
5459
|
-
|
|
5818
|
+
code: import_types16.BILLING_ERROR_CODES.ONBOARDING_CREATE_FAILED,
|
|
5819
|
+
message: reason ? `Onboarding creation failed: ${reason}` : "Onboarding creation failed",
|
|
5820
|
+
params: {
|
|
5821
|
+
reason
|
|
5822
|
+
}
|
|
5460
5823
|
});
|
|
5461
5824
|
}
|
|
5462
5825
|
} else {
|
|
@@ -5469,9 +5832,9 @@ var TenantBillingController = class _TenantBillingController {
|
|
|
5469
5832
|
});
|
|
5470
5833
|
if (dto.promoCode) {
|
|
5471
5834
|
if (!this.promoCodes) {
|
|
5472
|
-
warnings.push("
|
|
5835
|
+
warnings.push("A promo code was sent, but PromoCodesModule is not loaded \u2014 the code was NOT redeemed.");
|
|
5473
5836
|
} else if (!sub.id) {
|
|
5474
|
-
warnings.push("
|
|
5837
|
+
warnings.push("A promo code was sent, but the adapter returns no SubscriptionUsageRecord.id \u2014 the code was NOT redeemed.");
|
|
5475
5838
|
} else {
|
|
5476
5839
|
try {
|
|
5477
5840
|
const redemption = await this.promoCodes.redeem({
|
|
@@ -5482,8 +5845,8 @@ var TenantBillingController = class _TenantBillingController {
|
|
|
5482
5845
|
});
|
|
5483
5846
|
promoRedemption = this.toResponseRedemption(redemption, dto.promoCode);
|
|
5484
5847
|
} catch (err) {
|
|
5485
|
-
const message = err instanceof Error ? err.message : "
|
|
5486
|
-
warnings.push(`
|
|
5848
|
+
const message = err instanceof Error ? err.message : "Unknown error";
|
|
5849
|
+
warnings.push(`The promo code could not be redeemed: ${message}`);
|
|
5487
5850
|
}
|
|
5488
5851
|
}
|
|
5489
5852
|
}
|
|
@@ -5509,11 +5872,11 @@ var TenantBillingController = class _TenantBillingController {
|
|
|
5509
5872
|
});
|
|
5510
5873
|
bundlesAdded += 1;
|
|
5511
5874
|
} catch (err) {
|
|
5512
|
-
warnings.push(`Bundle '${bundleVersionId}'
|
|
5875
|
+
warnings.push(`Bundle '${bundleVersionId}' could not be booked: ` + (err instanceof Error ? err.message : String(err)));
|
|
5513
5876
|
}
|
|
5514
5877
|
}
|
|
5515
5878
|
} else if (bundleVersionIds.length > 0 && !this.subscriptionBundles) {
|
|
5516
|
-
warnings.push("Bundle
|
|
5879
|
+
warnings.push("Bundle bookings were requested during onboarding, but SubscriptionBundleModule is not registered in the consumer. No bundles were created.");
|
|
5517
5880
|
}
|
|
5518
5881
|
return {
|
|
5519
5882
|
plan: planResult.plan,
|
|
@@ -5533,9 +5896,20 @@ var TenantBillingController = class _TenantBillingController {
|
|
|
5533
5896
|
const tenantId = this.requireTenantId(req);
|
|
5534
5897
|
const userId = this.requireUserId(req);
|
|
5535
5898
|
const sub = await this.subscriptionUsage.findForTenant(tenantId);
|
|
5536
|
-
if (!sub)
|
|
5899
|
+
if (!sub) {
|
|
5900
|
+
throw new import_common36.NotFoundException({
|
|
5901
|
+
code: import_types16.BILLING_ERROR_CODES.SUBSCRIPTION_NOT_FOUND,
|
|
5902
|
+
message: `No subscription for tenant ${tenantId}`,
|
|
5903
|
+
params: {
|
|
5904
|
+
tenantId
|
|
5905
|
+
}
|
|
5906
|
+
});
|
|
5907
|
+
}
|
|
5537
5908
|
if (!sub.pendingPlanVersion) {
|
|
5538
|
-
throw new import_common36.BadRequestException(
|
|
5909
|
+
throw new import_common36.BadRequestException({
|
|
5910
|
+
code: import_types16.BILLING_ERROR_CODES.NO_PENDING_PLAN_VERSION,
|
|
5911
|
+
message: "There is no pending plan version awaiting confirmation."
|
|
5912
|
+
});
|
|
5539
5913
|
}
|
|
5540
5914
|
const result = await this.subscriptionWrite.acceptPendingPlanVersion(tenantId, userId, /* @__PURE__ */ new Date());
|
|
5541
5915
|
if (!result.alreadyAccepted) {
|
|
@@ -5580,7 +5954,10 @@ var TenantBillingController = class _TenantBillingController {
|
|
|
5580
5954
|
const resolver = this.tenantIdResolver ?? ((r) => r.user?.tenantId ?? null);
|
|
5581
5955
|
const tenantId = resolver(req);
|
|
5582
5956
|
if (!tenantId) {
|
|
5583
|
-
throw new import_common36.NotFoundException(
|
|
5957
|
+
throw new import_common36.NotFoundException({
|
|
5958
|
+
code: import_types16.AUTH_ERROR_CODES.TENANT_CONTEXT_MISSING,
|
|
5959
|
+
message: "No tenant ID found on the request"
|
|
5960
|
+
});
|
|
5584
5961
|
}
|
|
5585
5962
|
return tenantId;
|
|
5586
5963
|
}
|
|
@@ -5588,7 +5965,10 @@ var TenantBillingController = class _TenantBillingController {
|
|
|
5588
5965
|
const resolver = this.userIdResolver ?? ((r) => r.user?.sub ?? r.user?.id ?? null);
|
|
5589
5966
|
const userId = resolver(req);
|
|
5590
5967
|
if (!userId) {
|
|
5591
|
-
throw new import_common36.NotFoundException(
|
|
5968
|
+
throw new import_common36.NotFoundException({
|
|
5969
|
+
code: import_types16.AUTH_ERROR_CODES.TENANT_CONTEXT_MISSING,
|
|
5970
|
+
message: "No user ID found on the request"
|
|
5971
|
+
});
|
|
5592
5972
|
}
|
|
5593
5973
|
return userId;
|
|
5594
5974
|
}
|
|
@@ -5628,10 +6008,10 @@ var TenantBillingController = class _TenantBillingController {
|
|
|
5628
6008
|
collectPromoSkipReasons(promoCode, sub) {
|
|
5629
6009
|
const reasons = [];
|
|
5630
6010
|
if (!this.promoCodes) {
|
|
5631
|
-
reasons.push(`Promo-Code "${promoCode}"
|
|
6011
|
+
reasons.push(`Promo-Code "${promoCode}" was sent, but PromoCodesModule is not loaded \u2014 the code was NOT redeemed.`);
|
|
5632
6012
|
}
|
|
5633
6013
|
if (!sub.id) {
|
|
5634
|
-
reasons.push(`Promo-Code "${promoCode}"
|
|
6014
|
+
reasons.push(`Promo-Code "${promoCode}" was sent, but the adapter returns no SubscriptionUsageRecord.id \u2014 the code was NOT redeemed.`);
|
|
5635
6015
|
}
|
|
5636
6016
|
return reasons;
|
|
5637
6017
|
}
|
|
@@ -5646,7 +6026,7 @@ var TenantBillingController = class _TenantBillingController {
|
|
|
5646
6026
|
try {
|
|
5647
6027
|
await this.contractFreeze.freezeOnPlanChange(tenantId, plan, cycle, /* @__PURE__ */ new Date());
|
|
5648
6028
|
} catch (err) {
|
|
5649
|
-
this.logger.error(`Contract
|
|
6029
|
+
this.logger.error(`Contract freeze after plan change failed (tenant ${tenantId}): ${String(err)}`);
|
|
5650
6030
|
}
|
|
5651
6031
|
}
|
|
5652
6032
|
/**
|
|
@@ -5957,7 +6337,7 @@ __name(normalizeQuotas, "normalizeQuotas");
|
|
|
5957
6337
|
|
|
5958
6338
|
// src/billing/subscription-bundle-preview.service.ts
|
|
5959
6339
|
var import_common39 = require("@nestjs/common");
|
|
5960
|
-
var
|
|
6340
|
+
var import_types17 = require("@saasicat/types");
|
|
5961
6341
|
function _ts_decorate39(decorators, target, key, desc) {
|
|
5962
6342
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
5963
6343
|
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
@@ -5996,7 +6376,13 @@ var SubscriptionBundlePreviewService = class {
|
|
|
5996
6376
|
async previewAdd(ctx, input, now = /* @__PURE__ */ new Date()) {
|
|
5997
6377
|
const bundleVersion = await this.bundles.findVersionById(input.bundleVersionId);
|
|
5998
6378
|
if (!bundleVersion) {
|
|
5999
|
-
throw new import_common39.NotFoundException(
|
|
6379
|
+
throw new import_common39.NotFoundException({
|
|
6380
|
+
code: import_types17.CATALOG_ERROR_CODES.BUNDLE_VERSION_NOT_FOUND,
|
|
6381
|
+
message: `BundleVersion '${input.bundleVersionId}' not found`,
|
|
6382
|
+
params: {
|
|
6383
|
+
bundleVersionId: input.bundleVersionId
|
|
6384
|
+
}
|
|
6385
|
+
});
|
|
6000
6386
|
}
|
|
6001
6387
|
const blockers = [];
|
|
6002
6388
|
const warnings = [];
|
|
@@ -6005,7 +6391,7 @@ var SubscriptionBundlePreviewService = class {
|
|
|
6005
6391
|
if (activeBundleVersions.some((bv) => bv.id === bundleVersion.id)) {
|
|
6006
6392
|
blockers.push({
|
|
6007
6393
|
code: "BUNDLE_ALREADY_SUBSCRIBED",
|
|
6008
|
-
message: "
|
|
6394
|
+
message: "This bundle is already actively booked."
|
|
6009
6395
|
});
|
|
6010
6396
|
}
|
|
6011
6397
|
const planFeatures = new Set(await this.resolvePlanFeatures(ctx.currentPlanKey, now));
|
|
@@ -6013,14 +6399,14 @@ var SubscriptionBundlePreviewService = class {
|
|
|
6013
6399
|
if (redundantFeatures.length > 0) {
|
|
6014
6400
|
warnings.push({
|
|
6015
6401
|
code: "REDUNDANT_FEATURES",
|
|
6016
|
-
message: `${redundantFeatures.length}
|
|
6402
|
+
message: `${redundantFeatures.length} feature${redundantFeatures.length === 1 ? " is" : "s are"} already included in the plan or another booked bundle \u2014 the bundle would be paid for twice.`
|
|
6017
6403
|
});
|
|
6018
6404
|
}
|
|
6019
6405
|
const missingRequires = await this.collectMissingRequires(bundleVersion, planFeatures, activeBundleVersions);
|
|
6020
6406
|
if (missingRequires.length > 0) {
|
|
6021
6407
|
blockers.push({
|
|
6022
6408
|
code: "BUNDLE_FEATURE_DEPENDENCY_UNSATISFIED",
|
|
6023
|
-
message: `
|
|
6409
|
+
message: `The bundle requires [${missingRequires.join(", ")}] \u2014 present neither in the plan nor in the active bundles.`
|
|
6024
6410
|
});
|
|
6025
6411
|
}
|
|
6026
6412
|
const priceNet = resolveBundlePriceNet(bundleVersion, ctx.currentPlanKey, ctx.billingCycle);
|
|
@@ -6049,18 +6435,30 @@ var SubscriptionBundlePreviewService = class {
|
|
|
6049
6435
|
async previewCancel(ctx, input, now = /* @__PURE__ */ new Date()) {
|
|
6050
6436
|
const existing = await this.subscriptionBundles.findById(input.subscriptionBundleId);
|
|
6051
6437
|
if (!existing || existing.subscriptionId !== ctx.subscriptionId) {
|
|
6052
|
-
throw new import_common39.NotFoundException(
|
|
6438
|
+
throw new import_common39.NotFoundException({
|
|
6439
|
+
code: import_types17.BILLING_ERROR_CODES.SUBSCRIPTION_BUNDLE_NOT_FOUND,
|
|
6440
|
+
message: `SubscriptionBundle '${input.subscriptionBundleId}' not found`,
|
|
6441
|
+
params: {
|
|
6442
|
+
subscriptionBundleId: input.subscriptionBundleId
|
|
6443
|
+
}
|
|
6444
|
+
});
|
|
6053
6445
|
}
|
|
6054
6446
|
const bundleVersion = await this.bundles.findVersionById(existing.bundleVersionId);
|
|
6055
6447
|
if (!bundleVersion) {
|
|
6056
|
-
throw new import_common39.NotFoundException(
|
|
6448
|
+
throw new import_common39.NotFoundException({
|
|
6449
|
+
code: import_types17.CATALOG_ERROR_CODES.BUNDLE_VERSION_NOT_FOUND,
|
|
6450
|
+
message: `BundleVersion '${existing.bundleVersionId}' not found`,
|
|
6451
|
+
params: {
|
|
6452
|
+
bundleVersionId: existing.bundleVersionId
|
|
6453
|
+
}
|
|
6454
|
+
});
|
|
6057
6455
|
}
|
|
6058
6456
|
const blockers = [];
|
|
6059
6457
|
const warnings = [];
|
|
6060
6458
|
if (existing.canceledAt !== null) {
|
|
6061
6459
|
blockers.push({
|
|
6062
6460
|
code: "SUBSCRIPTION_BUNDLE_ALREADY_CANCELED",
|
|
6063
|
-
message: "
|
|
6461
|
+
message: "This bundle booking is already cancelled."
|
|
6064
6462
|
});
|
|
6065
6463
|
}
|
|
6066
6464
|
const effectiveAt = resolveBundleCancelEffectiveAt({
|
|
@@ -6072,7 +6470,7 @@ var SubscriptionBundlePreviewService = class {
|
|
|
6072
6470
|
if (existing.minimumTermEndsAt && existing.minimumTermEndsAt.getTime() > periodEnd.getTime()) {
|
|
6073
6471
|
warnings.push({
|
|
6074
6472
|
code: "MINIMUM_TERM_BINDS",
|
|
6075
|
-
message: "
|
|
6473
|
+
message: "The minimum term extends beyond the end of the period \u2014 the cancellation only takes effect when the minimum term ends."
|
|
6076
6474
|
});
|
|
6077
6475
|
}
|
|
6078
6476
|
return {
|
|
@@ -6091,26 +6489,26 @@ var SubscriptionBundlePreviewService = class {
|
|
|
6091
6489
|
if (bundleVersion.publishedAt === null) {
|
|
6092
6490
|
blockers.push({
|
|
6093
6491
|
code: "BUNDLE_VERSION_NOT_PUBLISHED",
|
|
6094
|
-
message: "
|
|
6492
|
+
message: "This bundle version is not published and cannot be booked."
|
|
6095
6493
|
});
|
|
6096
6494
|
}
|
|
6097
6495
|
if (bundleVersion.supersededAt !== null) {
|
|
6098
6496
|
blockers.push({
|
|
6099
6497
|
code: "BUNDLE_VERSION_SUPERSEDED",
|
|
6100
|
-
message: "
|
|
6498
|
+
message: "This bundle version has been superseded by a newer one."
|
|
6101
6499
|
});
|
|
6102
6500
|
}
|
|
6103
6501
|
const planIds = bundleVersion.compatibility?.planIds ?? [];
|
|
6104
6502
|
if (planIds.length > 0 && !planIds.includes(currentPlanKey)) {
|
|
6105
6503
|
blockers.push({
|
|
6106
6504
|
code: "BUNDLE_INCOMPATIBLE_WITH_PLAN",
|
|
6107
|
-
message: `
|
|
6505
|
+
message: `The bundle is not compatible with plan '${currentPlanKey}'. Allowed: [${planIds.join(", ")}].`
|
|
6108
6506
|
});
|
|
6109
6507
|
}
|
|
6110
6508
|
if (this.blockedBundles?.bundleKeys?.includes(bundleVersion.bundleKey)) {
|
|
6111
6509
|
blockers.push({
|
|
6112
6510
|
code: "BUNDLE_NOT_SELF_SERVICE",
|
|
6113
|
-
message: `Bundle '${bundleVersion.bundleKey}'
|
|
6511
|
+
message: `Bundle '${bundleVersion.bundleKey}' is only activated via a special contract. Please contact the contract manager.`
|
|
6114
6512
|
});
|
|
6115
6513
|
}
|
|
6116
6514
|
}
|
|
@@ -6159,12 +6557,12 @@ var SubscriptionBundlePreviewService = class {
|
|
|
6159
6557
|
const entries = await this.catalogEntries.listFeatures({
|
|
6160
6558
|
projectKey
|
|
6161
6559
|
});
|
|
6162
|
-
const requiresIndex = (0,
|
|
6560
|
+
const requiresIndex = (0, import_types17.buildFeatureRequiresIndex)(entries);
|
|
6163
6561
|
const covered = /* @__PURE__ */ new Set([
|
|
6164
6562
|
...planFeatures,
|
|
6165
6563
|
...activeBundleVersions.flatMap((bv) => bv.features ?? [])
|
|
6166
6564
|
]);
|
|
6167
|
-
return (0,
|
|
6565
|
+
return (0, import_types17.collectUnsatisfiedRequires)(bundleVersion.features ?? [], requiresIndex).filter((key) => !covered.has(key));
|
|
6168
6566
|
}
|
|
6169
6567
|
/** `projectKey` lives on the bundle stem, not on the version. */
|
|
6170
6568
|
async resolveProjectKey(bundleVersion) {
|
|
@@ -6223,6 +6621,7 @@ var import_common41 = require("@nestjs/common");
|
|
|
6223
6621
|
|
|
6224
6622
|
// src/billing/tenant-subscription-bundles.controller.ts
|
|
6225
6623
|
var import_common40 = require("@nestjs/common");
|
|
6624
|
+
var import_types18 = require("@saasicat/types");
|
|
6226
6625
|
|
|
6227
6626
|
// src/billing/dto/subscription-bundles.dto.ts
|
|
6228
6627
|
var import_class_validator5 = require("class-validator");
|
|
@@ -6303,7 +6702,7 @@ var CancelSubscriptionBundleDto = class {
|
|
|
6303
6702
|
_ts_decorate40([
|
|
6304
6703
|
(0, import_class_validator5.IsOptional)(),
|
|
6305
6704
|
(0, import_class_validator5.Matches)(ISO_DATE_PATTERN, {
|
|
6306
|
-
message: "canceledAt
|
|
6705
|
+
message: "canceledAt must be an ISO date (YYYY-MM-DD)"
|
|
6307
6706
|
}),
|
|
6308
6707
|
_ts_metadata28("design:type", String)
|
|
6309
6708
|
], CancelSubscriptionBundleDto.prototype, "canceledAt", void 0);
|
|
@@ -6367,7 +6766,10 @@ function buildTenantSubscriptionBundlesController(extraGuards = []) {
|
|
|
6367
6766
|
const hasAdd = Boolean(dto.bundleVersionId);
|
|
6368
6767
|
const hasCancel = Boolean(dto.subscriptionBundleId);
|
|
6369
6768
|
if (hasAdd === hasCancel) {
|
|
6370
|
-
throw new import_common40.BadRequestException(
|
|
6769
|
+
throw new import_common40.BadRequestException({
|
|
6770
|
+
code: import_types18.BILLING_ERROR_CODES.BUNDLE_PREVIEW_ARGUMENT_AMBIGUOUS,
|
|
6771
|
+
message: "Exactly one of bundleVersionId (add preview) or subscriptionBundleId (cancel preview) must be given."
|
|
6772
|
+
});
|
|
6371
6773
|
}
|
|
6372
6774
|
const sub = await this.requireSubscription(this.requireTenantId(req));
|
|
6373
6775
|
const ctx = {
|
|
@@ -6407,21 +6809,30 @@ function buildTenantSubscriptionBundlesController(extraGuards = []) {
|
|
|
6407
6809
|
}
|
|
6408
6810
|
requireSubscriptionPk(sub) {
|
|
6409
6811
|
if (!sub.id) {
|
|
6410
|
-
throw new import_common40.NotFoundException("SubscriptionUsageRecord
|
|
6812
|
+
throw new import_common40.NotFoundException("SubscriptionUsageRecord without `id` \u2014 the adapter must return the subscription primary key (see SubscriptionUsageRecord.id).");
|
|
6411
6813
|
}
|
|
6412
6814
|
return sub.id;
|
|
6413
6815
|
}
|
|
6414
6816
|
requireTenantId(req) {
|
|
6415
6817
|
const tenantId = this.tenantIdResolver(req);
|
|
6416
6818
|
if (!tenantId) {
|
|
6417
|
-
throw new import_common40.NotFoundException(
|
|
6819
|
+
throw new import_common40.NotFoundException({
|
|
6820
|
+
code: import_types18.AUTH_ERROR_CODES.TENANT_CONTEXT_MISSING,
|
|
6821
|
+
message: "No tenant ID found on the request"
|
|
6822
|
+
});
|
|
6418
6823
|
}
|
|
6419
6824
|
return tenantId;
|
|
6420
6825
|
}
|
|
6421
6826
|
async requireSubscription(tenantId) {
|
|
6422
6827
|
const sub = await this.subscriptionUsage.findForTenant(tenantId);
|
|
6423
6828
|
if (!sub) {
|
|
6424
|
-
throw new import_common40.NotFoundException(
|
|
6829
|
+
throw new import_common40.NotFoundException({
|
|
6830
|
+
code: import_types18.BILLING_ERROR_CODES.SUBSCRIPTION_NOT_FOUND,
|
|
6831
|
+
message: `No subscription for tenant ${tenantId}`,
|
|
6832
|
+
params: {
|
|
6833
|
+
tenantId
|
|
6834
|
+
}
|
|
6835
|
+
});
|
|
6425
6836
|
}
|
|
6426
6837
|
return sub;
|
|
6427
6838
|
}
|
|
@@ -6440,7 +6851,7 @@ function buildTenantSubscriptionBundlesController(extraGuards = []) {
|
|
|
6440
6851
|
try {
|
|
6441
6852
|
await this.contractFreeze.freezeOnPlanChange(tenantId, sub.planVersion.planId, sub.billingCycle, /* @__PURE__ */ new Date());
|
|
6442
6853
|
} catch (err) {
|
|
6443
|
-
this.logger.error(`Contract
|
|
6854
|
+
this.logger.error(`Contract freeze after bundle mutation failed (tenant ${tenantId}): ${String(err)}`);
|
|
6444
6855
|
}
|
|
6445
6856
|
}
|
|
6446
6857
|
};
|
|
@@ -6659,7 +7070,7 @@ __export(catalog_exports, {
|
|
|
6659
7070
|
|
|
6660
7071
|
// src/catalog/bundles.service.ts
|
|
6661
7072
|
var import_common43 = require("@nestjs/common");
|
|
6662
|
-
var
|
|
7073
|
+
var import_types19 = require("@saasicat/types");
|
|
6663
7074
|
|
|
6664
7075
|
// src/discovery/discovery.scanner.ts
|
|
6665
7076
|
var import_node_crypto2 = require("crypto");
|
|
@@ -6728,7 +7139,7 @@ var DiscoveryScanner = class _DiscoveryScanner {
|
|
|
6728
7139
|
(0, import_node_fs2.writeFileSync)(path, JSON.stringify(snapshot, null, 2) + "\n", "utf-8");
|
|
6729
7140
|
this.logger.log(`Discovery snapshot persisted to ${path}`);
|
|
6730
7141
|
} catch (err) {
|
|
6731
|
-
this.logger.warn(`
|
|
7142
|
+
this.logger.warn(`The discovery snapshot could not be written to ${path}: ${err.message}`);
|
|
6732
7143
|
}
|
|
6733
7144
|
}
|
|
6734
7145
|
/**
|
|
@@ -6813,7 +7224,7 @@ var DiscoveryScanner = class _DiscoveryScanner {
|
|
|
6813
7224
|
registerCapability(target, meta, declaredAt) {
|
|
6814
7225
|
const existing = target.get(meta.capabilityKey);
|
|
6815
7226
|
if (existing) {
|
|
6816
|
-
this.logger.warn(`Capability '${meta.capabilityKey}'
|
|
7227
|
+
this.logger.warn(`Capability '${meta.capabilityKey}' is declared in several places (${existing.declaredAt} and ${declaredAt}); the first declaration wins`);
|
|
6817
7228
|
return;
|
|
6818
7229
|
}
|
|
6819
7230
|
target.set(meta.capabilityKey, {
|
|
@@ -6834,7 +7245,7 @@ var DiscoveryScanner = class _DiscoveryScanner {
|
|
|
6834
7245
|
registerQuota(target, opts, declaredAt) {
|
|
6835
7246
|
const existing = target.get(opts.key);
|
|
6836
7247
|
if (existing) {
|
|
6837
|
-
this.logger.warn(`Quota '${opts.key}'
|
|
7248
|
+
this.logger.warn(`Quota '${opts.key}' is defined in several places (${existing.declaredAt} and ${declaredAt}); the first definition wins`);
|
|
6838
7249
|
return;
|
|
6839
7250
|
}
|
|
6840
7251
|
target.set(opts.key, {
|
|
@@ -7013,7 +7424,7 @@ function dependencyWarnings(features, snapshot, code) {
|
|
|
7013
7424
|
if (selected.has(required)) continue;
|
|
7014
7425
|
warnings.push({
|
|
7015
7426
|
code,
|
|
7016
|
-
message: `Feature '${feature}'
|
|
7427
|
+
message: `Feature '${feature}' requires '${required}', which is missing from this selection. The tenant must cover that dependency some other way (for example through the plan) \u2014 otherwise the feature will not work.`,
|
|
7017
7428
|
field: `features[${index}]`,
|
|
7018
7429
|
value: required
|
|
7019
7430
|
});
|
|
@@ -7032,7 +7443,7 @@ function validateBundleDraft(draft, snapshot, knownPlanKeys = null, marketedOnly
|
|
|
7032
7443
|
if (approved && !approved.features.has(feature)) {
|
|
7033
7444
|
warnings.push({
|
|
7034
7445
|
code: "BUNDLE_FEATURE_NOT_APPROVED",
|
|
7035
|
-
message: `Feature '${feature}'
|
|
7446
|
+
message: `Feature '${feature}' is not approved (status != approved). Only approved features are sellable \u2014 approve it in the discovery review (#20).`,
|
|
7036
7447
|
field: `features[${index}]`,
|
|
7037
7448
|
value: feature
|
|
7038
7449
|
});
|
|
@@ -7041,7 +7452,7 @@ function validateBundleDraft(draft, snapshot, knownPlanKeys = null, marketedOnly
|
|
|
7041
7452
|
}
|
|
7042
7453
|
warnings.push({
|
|
7043
7454
|
code: "BUNDLE_FEATURE_UNKNOWN",
|
|
7044
|
-
message: `Feature '${feature}'
|
|
7455
|
+
message: `Feature '${feature}' was not found in the discovery snapshot. Either the code is missing an @ImplementsCapability(..., { feature: '${feature}' }) annotation, or the feature is obsolete.`,
|
|
7045
7456
|
field: `features[${index}]`,
|
|
7046
7457
|
value: feature
|
|
7047
7458
|
});
|
|
@@ -7050,14 +7461,14 @@ function validateBundleDraft(draft, snapshot, knownPlanKeys = null, marketedOnly
|
|
|
7050
7461
|
if (!knownQuotas.has(quotaKey)) {
|
|
7051
7462
|
warnings.push({
|
|
7052
7463
|
code: "QUOTA_MISSING",
|
|
7053
|
-
message: `Quota '${quotaKey}'
|
|
7464
|
+
message: `Quota '${quotaKey}' was not found in the discovery snapshot. In code, a class must be annotated with @DefinesQuota({ key: '${quotaKey}', ... }).`,
|
|
7054
7465
|
field: `quotas.${quotaKey}`,
|
|
7055
7466
|
value: quotaKey
|
|
7056
7467
|
});
|
|
7057
7468
|
} else if (approved && !approved.quotas.has(quotaKey)) {
|
|
7058
7469
|
warnings.push({
|
|
7059
7470
|
code: "QUOTA_NOT_APPROVED",
|
|
7060
|
-
message: `Quota '${quotaKey}'
|
|
7471
|
+
message: `Quota '${quotaKey}' is not approved (status != approved). Only approved quotas are sellable \u2014 approve it in the discovery review (#20).`,
|
|
7061
7472
|
field: `quotas.${quotaKey}`,
|
|
7062
7473
|
value: quotaKey
|
|
7063
7474
|
});
|
|
@@ -7069,7 +7480,7 @@ function validateBundleDraft(draft, snapshot, knownPlanKeys = null, marketedOnly
|
|
|
7069
7480
|
if (knownPlanKeys.has(planKey)) return;
|
|
7070
7481
|
warnings.push({
|
|
7071
7482
|
code: "BUNDLE_PLAN_KEY_UNKNOWN",
|
|
7072
|
-
message: `Plan '${planKey}'
|
|
7483
|
+
message: `Plan '${planKey}' from compatibility.planIds does not exist in the project. Either fix the planKey, or create the plan record first.`,
|
|
7073
7484
|
field: `compatibility.planIds[${index}]`,
|
|
7074
7485
|
value: planKey
|
|
7075
7486
|
});
|
|
@@ -7089,7 +7500,7 @@ function validatePlanDraft(draft, snapshot, marketedOnlyFeatures = /* @__PURE__
|
|
|
7089
7500
|
if (approved && !approved.features.has(feature)) {
|
|
7090
7501
|
warnings.push({
|
|
7091
7502
|
code: "PLAN_FEATURE_NOT_APPROVED",
|
|
7092
|
-
message: `Feature '${feature}'
|
|
7503
|
+
message: `Feature '${feature}' is not approved (status != approved). Only approved features are sellable \u2014 approve it in the discovery review (#20).`,
|
|
7093
7504
|
field: `features[${index}]`,
|
|
7094
7505
|
value: feature
|
|
7095
7506
|
});
|
|
@@ -7098,7 +7509,7 @@ function validatePlanDraft(draft, snapshot, marketedOnlyFeatures = /* @__PURE__
|
|
|
7098
7509
|
}
|
|
7099
7510
|
warnings.push({
|
|
7100
7511
|
code: "PLAN_FEATURE_UNKNOWN",
|
|
7101
|
-
message: `Feature '${feature}'
|
|
7512
|
+
message: `Feature '${feature}' was not found in the discovery snapshot. Either the code is missing an @ImplementsCapability(..., { feature: '${feature}' }) annotation, or the feature is obsolete.`,
|
|
7102
7513
|
field: `features[${index}]`,
|
|
7103
7514
|
value: feature
|
|
7104
7515
|
});
|
|
@@ -7107,14 +7518,14 @@ function validatePlanDraft(draft, snapshot, marketedOnlyFeatures = /* @__PURE__
|
|
|
7107
7518
|
if (!knownQuotas.has(quotaKey)) {
|
|
7108
7519
|
warnings.push({
|
|
7109
7520
|
code: "QUOTA_MISSING",
|
|
7110
|
-
message: `Quota '${quotaKey}'
|
|
7521
|
+
message: `Quota '${quotaKey}' was not found in the discovery snapshot. In code, a class must be annotated with @DefinesQuota({ key: '${quotaKey}', ... }).`,
|
|
7111
7522
|
field: `quotas.${quotaKey}`,
|
|
7112
7523
|
value: quotaKey
|
|
7113
7524
|
});
|
|
7114
7525
|
} else if (approved && !approved.quotas.has(quotaKey)) {
|
|
7115
7526
|
warnings.push({
|
|
7116
7527
|
code: "QUOTA_NOT_APPROVED",
|
|
7117
|
-
message: `Quota '${quotaKey}'
|
|
7528
|
+
message: `Quota '${quotaKey}' is not approved (status != approved). Only approved quotas are sellable \u2014 approve it in the discovery review (#20).`,
|
|
7118
7529
|
field: `quotas.${quotaKey}`,
|
|
7119
7530
|
value: quotaKey
|
|
7120
7531
|
});
|
|
@@ -7166,7 +7577,7 @@ var BundlesService = class _BundlesService {
|
|
|
7166
7577
|
this.mode = config.strictModeCheckMode ?? "blocking";
|
|
7167
7578
|
this.marketedOnly = new Set(config.marketedOnlyFeatures ?? []);
|
|
7168
7579
|
if (this.mode === "blocking" && !hasDiscoverySnapshotSource(this.snapshot, this.scanner)) {
|
|
7169
|
-
this.logger.error("BundlesService: strictModeCheckMode=blocking
|
|
7580
|
+
this.logger.error("BundlesService: strictModeCheckMode=blocking without a DiscoverySnapshot source \u2014 degrading to warn-only. Wire DiscoveryModule so that enforcement takes effect (#25).");
|
|
7170
7581
|
this.mode = "warn-only";
|
|
7171
7582
|
}
|
|
7172
7583
|
}
|
|
@@ -7181,7 +7592,15 @@ var BundlesService = class _BundlesService {
|
|
|
7181
7592
|
}
|
|
7182
7593
|
async getBundle(bundleId) {
|
|
7183
7594
|
const bundle = await this.repo.findById(bundleId);
|
|
7184
|
-
if (!bundle)
|
|
7595
|
+
if (!bundle) {
|
|
7596
|
+
throw new import_common43.NotFoundException({
|
|
7597
|
+
code: import_types19.CATALOG_ERROR_CODES.BUNDLE_NOT_FOUND,
|
|
7598
|
+
message: `Bundle '${bundleId}' not found`,
|
|
7599
|
+
params: {
|
|
7600
|
+
bundleId
|
|
7601
|
+
}
|
|
7602
|
+
});
|
|
7603
|
+
}
|
|
7185
7604
|
const versions = await this.annotateEditability(await this.repo.listVersions(bundleId));
|
|
7186
7605
|
return {
|
|
7187
7606
|
bundle,
|
|
@@ -7191,18 +7610,41 @@ var BundlesService = class _BundlesService {
|
|
|
7191
7610
|
async createBundle(data) {
|
|
7192
7611
|
const existing = await this.repo.findByKey(data.projectKey, data.bundleKey);
|
|
7193
7612
|
if (existing) {
|
|
7194
|
-
throw new import_common43.UnprocessableEntityException(
|
|
7613
|
+
throw new import_common43.UnprocessableEntityException({
|
|
7614
|
+
code: import_types19.CATALOG_ERROR_CODES.BUNDLE_ALREADY_EXISTS,
|
|
7615
|
+
message: `Bundle '${data.bundleKey}' already exists in project '${data.projectKey}'`,
|
|
7616
|
+
params: {
|
|
7617
|
+
bundleKey: data.bundleKey,
|
|
7618
|
+
projectKey: data.projectKey
|
|
7619
|
+
}
|
|
7620
|
+
});
|
|
7195
7621
|
}
|
|
7196
7622
|
return this.repo.create(data);
|
|
7197
7623
|
}
|
|
7198
7624
|
async updateBundle(bundleId, data) {
|
|
7199
7625
|
const existing = await this.repo.findById(bundleId);
|
|
7200
|
-
if (!existing)
|
|
7626
|
+
if (!existing) {
|
|
7627
|
+
throw new import_common43.NotFoundException({
|
|
7628
|
+
code: import_types19.CATALOG_ERROR_CODES.BUNDLE_NOT_FOUND,
|
|
7629
|
+
message: `Bundle '${bundleId}' not found`,
|
|
7630
|
+
params: {
|
|
7631
|
+
bundleId
|
|
7632
|
+
}
|
|
7633
|
+
});
|
|
7634
|
+
}
|
|
7201
7635
|
return this.repo.update(bundleId, data);
|
|
7202
7636
|
}
|
|
7203
7637
|
async softDeleteBundle(bundleId) {
|
|
7204
7638
|
const existing = await this.repo.findById(bundleId);
|
|
7205
|
-
if (!existing)
|
|
7639
|
+
if (!existing) {
|
|
7640
|
+
throw new import_common43.NotFoundException({
|
|
7641
|
+
code: import_types19.CATALOG_ERROR_CODES.BUNDLE_NOT_FOUND,
|
|
7642
|
+
message: `Bundle '${bundleId}' not found`,
|
|
7643
|
+
params: {
|
|
7644
|
+
bundleId
|
|
7645
|
+
}
|
|
7646
|
+
});
|
|
7647
|
+
}
|
|
7206
7648
|
if (existing.deletedAt !== null) return;
|
|
7207
7649
|
await this.repo.softDelete(bundleId);
|
|
7208
7650
|
}
|
|
@@ -7214,18 +7656,27 @@ var BundlesService = class _BundlesService {
|
|
|
7214
7656
|
async discardBundleDraft(versionId) {
|
|
7215
7657
|
const existing = await this.repo.findVersionById(versionId);
|
|
7216
7658
|
if (!existing) {
|
|
7217
|
-
throw new import_common43.NotFoundException(
|
|
7659
|
+
throw new import_common43.NotFoundException({
|
|
7660
|
+
code: import_types19.CATALOG_ERROR_CODES.BUNDLE_VERSION_NOT_FOUND,
|
|
7661
|
+
message: `BundleVersion '${versionId}' not found`,
|
|
7662
|
+
params: {
|
|
7663
|
+
versionId
|
|
7664
|
+
}
|
|
7665
|
+
});
|
|
7218
7666
|
}
|
|
7219
7667
|
if (existing.publishedAt !== null) {
|
|
7220
7668
|
throw new import_common43.UnprocessableEntityException({
|
|
7221
|
-
code:
|
|
7222
|
-
message: `BundleVersion '${versionId}'
|
|
7669
|
+
code: import_types19.CATALOG_ERROR_CODES.BUNDLE_VERSION_ALREADY_PUBLISHED,
|
|
7670
|
+
message: `BundleVersion '${versionId}' is already published and cannot be discarded.`,
|
|
7671
|
+
params: {
|
|
7672
|
+
versionId
|
|
7673
|
+
}
|
|
7223
7674
|
});
|
|
7224
7675
|
}
|
|
7225
7676
|
if (typeof this.repo.deleteDraft !== "function") {
|
|
7226
7677
|
throw new import_common43.UnprocessableEntityException({
|
|
7227
|
-
code:
|
|
7228
|
-
message: "Discard
|
|
7678
|
+
code: import_types19.CATALOG_ERROR_CODES.BUNDLE_VERSION_DISCARD_NOT_IMPLEMENTED,
|
|
7679
|
+
message: "Discard is not implemented in the current repository. Implement BundleRepository.deleteDraft."
|
|
7229
7680
|
});
|
|
7230
7681
|
}
|
|
7231
7682
|
await this.repo.deleteDraft(versionId);
|
|
@@ -7239,7 +7690,13 @@ var BundlesService = class _BundlesService {
|
|
|
7239
7690
|
async getBundleVersion(versionId) {
|
|
7240
7691
|
const version = await this.repo.findVersionById(versionId);
|
|
7241
7692
|
if (!version) {
|
|
7242
|
-
throw new import_common43.NotFoundException(
|
|
7693
|
+
throw new import_common43.NotFoundException({
|
|
7694
|
+
code: import_types19.CATALOG_ERROR_CODES.BUNDLE_VERSION_NOT_FOUND,
|
|
7695
|
+
message: `BundleVersion '${versionId}' not found`,
|
|
7696
|
+
params: {
|
|
7697
|
+
versionId
|
|
7698
|
+
}
|
|
7699
|
+
});
|
|
7243
7700
|
}
|
|
7244
7701
|
const [annotated] = await this.annotateEditability([
|
|
7245
7702
|
version
|
|
@@ -7281,11 +7738,24 @@ var BundlesService = class _BundlesService {
|
|
|
7281
7738
|
async createBundleDraft(data) {
|
|
7282
7739
|
const bundle = await this.repo.findById(data.bundleId);
|
|
7283
7740
|
if (!bundle) {
|
|
7284
|
-
throw new import_common43.NotFoundException(
|
|
7741
|
+
throw new import_common43.NotFoundException({
|
|
7742
|
+
code: import_types19.CATALOG_ERROR_CODES.BUNDLE_NOT_FOUND,
|
|
7743
|
+
message: `Bundle '${data.bundleId}' not found`,
|
|
7744
|
+
params: {
|
|
7745
|
+
bundleId: data.bundleId
|
|
7746
|
+
}
|
|
7747
|
+
});
|
|
7285
7748
|
}
|
|
7286
7749
|
const existingDraft = await this.repo.findCurrentDraft(data.bundleId);
|
|
7287
7750
|
if (existingDraft) {
|
|
7288
|
-
throw new import_common43.UnprocessableEntityException(
|
|
7751
|
+
throw new import_common43.UnprocessableEntityException({
|
|
7752
|
+
code: import_types19.CATALOG_ERROR_CODES.BUNDLE_DRAFT_ALREADY_EXISTS,
|
|
7753
|
+
message: `Bundle '${bundle.bundleKey}' already has a draft version v${existingDraft.version}; publish or discard it first`,
|
|
7754
|
+
params: {
|
|
7755
|
+
bundleKey: bundle.bundleKey,
|
|
7756
|
+
draftVersion: existingDraft.version
|
|
7757
|
+
}
|
|
7758
|
+
});
|
|
7289
7759
|
}
|
|
7290
7760
|
if (data.baseVersionId === void 0) {
|
|
7291
7761
|
const latestLive = await this.repo.findLatestLive(data.bundleId);
|
|
@@ -7317,16 +7787,25 @@ var BundlesService = class _BundlesService {
|
|
|
7317
7787
|
async updateBundleDraft(versionId, data) {
|
|
7318
7788
|
const existing = await this.repo.findVersionById(versionId);
|
|
7319
7789
|
if (!existing) {
|
|
7320
|
-
throw new import_common43.NotFoundException(
|
|
7790
|
+
throw new import_common43.NotFoundException({
|
|
7791
|
+
code: import_types19.CATALOG_ERROR_CODES.BUNDLE_VERSION_NOT_FOUND,
|
|
7792
|
+
message: `BundleVersion '${versionId}' not found`,
|
|
7793
|
+
params: {
|
|
7794
|
+
versionId
|
|
7795
|
+
}
|
|
7796
|
+
});
|
|
7321
7797
|
}
|
|
7322
7798
|
const [annotated] = await this.annotateEditability([
|
|
7323
7799
|
existing
|
|
7324
7800
|
]);
|
|
7325
|
-
const { editable } = (0,
|
|
7801
|
+
const { editable } = (0, import_types19.isVersionEditable)(annotated);
|
|
7326
7802
|
if (!editable) {
|
|
7327
7803
|
throw new import_common43.UnprocessableEntityException({
|
|
7328
|
-
code:
|
|
7329
|
-
message: `BundleVersion '${versionId}'
|
|
7804
|
+
code: import_types19.CATALOG_ERROR_CODES.BUNDLE_VERSION_NOT_EDITABLE,
|
|
7805
|
+
message: `BundleVersion '${versionId}' is not editable. Only drafts and published versions are editable that are latest-in-chain, bind no subscription yet, and whose validFrom lies in the future.`,
|
|
7806
|
+
params: {
|
|
7807
|
+
versionId
|
|
7808
|
+
}
|
|
7330
7809
|
});
|
|
7331
7810
|
}
|
|
7332
7811
|
const merged = {
|
|
@@ -7357,10 +7836,22 @@ var BundlesService = class _BundlesService {
|
|
|
7357
7836
|
async publishBundleVersion(versionId, publishMeta) {
|
|
7358
7837
|
const draft = await this.repo.findVersionById(versionId);
|
|
7359
7838
|
if (!draft) {
|
|
7360
|
-
throw new import_common43.NotFoundException(
|
|
7839
|
+
throw new import_common43.NotFoundException({
|
|
7840
|
+
code: import_types19.CATALOG_ERROR_CODES.BUNDLE_VERSION_NOT_FOUND,
|
|
7841
|
+
message: `BundleVersion '${versionId}' not found`,
|
|
7842
|
+
params: {
|
|
7843
|
+
versionId
|
|
7844
|
+
}
|
|
7845
|
+
});
|
|
7361
7846
|
}
|
|
7362
7847
|
if (draft.publishedAt !== null) {
|
|
7363
|
-
throw new import_common43.UnprocessableEntityException(
|
|
7848
|
+
throw new import_common43.UnprocessableEntityException({
|
|
7849
|
+
code: import_types19.CATALOG_ERROR_CODES.BUNDLE_VERSION_ALREADY_PUBLISHED,
|
|
7850
|
+
message: `BundleVersion '${versionId}' is already published`,
|
|
7851
|
+
params: {
|
|
7852
|
+
versionId
|
|
7853
|
+
}
|
|
7854
|
+
});
|
|
7364
7855
|
}
|
|
7365
7856
|
const bundle = await this.repo.findById(draft.bundleId);
|
|
7366
7857
|
const warnings = await this.runStrictCheck({
|
|
@@ -7373,8 +7864,8 @@ var BundlesService = class _BundlesService {
|
|
|
7373
7864
|
const explicitZero = /* @__PURE__ */ __name((v) => v !== null && v !== void 0 && Number.parseFloat(String(v)) <= 0, "explicitZero");
|
|
7374
7865
|
if (explicitZero(draft.monthlyNet) || explicitZero(draft.yearlyNet)) {
|
|
7375
7866
|
throw new import_common43.UnprocessableEntityException({
|
|
7376
|
-
code:
|
|
7377
|
-
message: "
|
|
7867
|
+
code: import_types19.CATALOG_ERROR_CODES.BUNDLE_VERSION_ZERO_PRICE,
|
|
7868
|
+
message: "A bundle version cannot be published with an explicit price of 0.00 (guard against seed placeholders). For free bundles leave it null, or set allowZeroPrice.",
|
|
7378
7869
|
monthlyNet: draft.monthlyNet,
|
|
7379
7870
|
yearlyNet: draft.yearlyNet
|
|
7380
7871
|
});
|
|
@@ -7384,23 +7875,30 @@ var BundlesService = class _BundlesService {
|
|
|
7384
7875
|
const validFromInput = publishMeta.validFrom ?? draft.validFrom;
|
|
7385
7876
|
if (!validFromInput) {
|
|
7386
7877
|
throw new import_common43.UnprocessableEntityException({
|
|
7387
|
-
code:
|
|
7388
|
-
message: "
|
|
7878
|
+
code: import_types19.CATALOG_ERROR_CODES.BUNDLE_VERSION_VALID_FROM_REQUIRED,
|
|
7879
|
+
message: "validFrom must be set when publishing (on the draft or the publish call). SPEC_V2 \xA74.2."
|
|
7389
7880
|
});
|
|
7390
7881
|
}
|
|
7391
7882
|
const validFrom = new Date(validFromInput);
|
|
7392
7883
|
if (Number.isNaN(validFrom.getTime())) {
|
|
7393
7884
|
throw new import_common43.UnprocessableEntityException({
|
|
7394
|
-
code:
|
|
7395
|
-
message: `validFrom '${validFromInput}'
|
|
7885
|
+
code: import_types19.CATALOG_ERROR_CODES.BUNDLE_VERSION_VALID_FROM_INVALID,
|
|
7886
|
+
message: `validFrom '${validFromInput}' is not a valid date`,
|
|
7887
|
+
params: {
|
|
7888
|
+
validFrom: validFromInput
|
|
7889
|
+
}
|
|
7396
7890
|
});
|
|
7397
7891
|
}
|
|
7398
7892
|
if (previous?.validFrom) {
|
|
7399
7893
|
const prevFrom = new Date(previous.validFrom);
|
|
7400
7894
|
if (validFrom <= prevFrom) {
|
|
7401
7895
|
throw new import_common43.UnprocessableEntityException({
|
|
7402
|
-
code:
|
|
7403
|
-
message: `validFrom (${validFrom.toISOString()})
|
|
7896
|
+
code: import_types19.CATALOG_ERROR_CODES.BUNDLE_VERSION_VALID_FROM_NOT_AFTER_PREVIOUS,
|
|
7897
|
+
message: `validFrom (${validFrom.toISOString()}) must be strictly after the validFrom of the previous version (${previous.validFrom}).`,
|
|
7898
|
+
params: {
|
|
7899
|
+
validFrom: validFrom.toISOString(),
|
|
7900
|
+
previousValidFrom: previous.validFrom
|
|
7901
|
+
}
|
|
7404
7902
|
});
|
|
7405
7903
|
}
|
|
7406
7904
|
if (previous.validUntil) {
|
|
@@ -7409,8 +7907,11 @@ var BundlesService = class _BundlesService {
|
|
|
7409
7907
|
const requiredStart = new Date(prevUntil.getTime() + dayMs);
|
|
7410
7908
|
if (validFrom.getTime() !== requiredStart.getTime()) {
|
|
7411
7909
|
throw new import_common43.UnprocessableEntityException({
|
|
7412
|
-
code:
|
|
7413
|
-
message: `
|
|
7910
|
+
code: import_types19.CATALOG_ERROR_CODES.BUNDLE_VERSION_VALID_FROM_NOT_GAPLESS,
|
|
7911
|
+
message: `The predecessor has validUntil=${previous.validUntil.slice(0, 10)} \u2014 the successor must start seamlessly on the next day (${requiredStart.toISOString().slice(0, 10)}). Received: ${validFrom.toISOString().slice(0, 10)}.`,
|
|
7912
|
+
params: {
|
|
7913
|
+
received: validFrom.toISOString().slice(0, 10)
|
|
7914
|
+
},
|
|
7414
7915
|
requiredValidFrom: requiredStart.toISOString().slice(0, 10),
|
|
7415
7916
|
previousValidUntil: previous.validUntil
|
|
7416
7917
|
});
|
|
@@ -7421,17 +7922,24 @@ var BundlesService = class _BundlesService {
|
|
|
7421
7922
|
const validUntil = validUntilInput ? new Date(validUntilInput) : null;
|
|
7422
7923
|
if (validUntil && Number.isNaN(validUntil.getTime())) {
|
|
7423
7924
|
throw new import_common43.UnprocessableEntityException({
|
|
7424
|
-
code:
|
|
7425
|
-
message: `validUntil '${validUntilInput}'
|
|
7925
|
+
code: import_types19.CATALOG_ERROR_CODES.BUNDLE_VERSION_VALID_UNTIL_INVALID,
|
|
7926
|
+
message: `validUntil '${validUntilInput}' is not a valid date`,
|
|
7927
|
+
params: {
|
|
7928
|
+
validUntil: validUntilInput
|
|
7929
|
+
}
|
|
7426
7930
|
});
|
|
7427
7931
|
}
|
|
7428
7932
|
if (validUntil && validUntil <= validFrom) {
|
|
7429
7933
|
throw new import_common43.UnprocessableEntityException({
|
|
7430
|
-
code:
|
|
7431
|
-
message: `validUntil (${validUntil.toISOString()})
|
|
7934
|
+
code: import_types19.CATALOG_ERROR_CODES.BUNDLE_VERSION_VALID_UNTIL_BEFORE_FROM,
|
|
7935
|
+
message: `validUntil (${validUntil.toISOString()}) must be strictly after validFrom (${validFrom.toISOString()}).`,
|
|
7936
|
+
params: {
|
|
7937
|
+
validUntil: validUntil.toISOString(),
|
|
7938
|
+
validFrom: validFrom.toISOString()
|
|
7939
|
+
}
|
|
7432
7940
|
});
|
|
7433
7941
|
}
|
|
7434
|
-
const diff = previous ? (0,
|
|
7942
|
+
const diff = previous ? (0, import_types4.classifyBundleVersionDiff)({
|
|
7435
7943
|
features: previous.features,
|
|
7436
7944
|
quotas: previous.quotas,
|
|
7437
7945
|
monthlyNet: previous.monthlyNet,
|
|
@@ -7447,8 +7955,8 @@ var BundlesService = class _BundlesService {
|
|
|
7447
7955
|
};
|
|
7448
7956
|
if (!diff.nonRegressive && !publishMeta.forceRegressive) {
|
|
7449
7957
|
throw new import_common43.UnprocessableEntityException({
|
|
7450
|
-
code:
|
|
7451
|
-
message: "
|
|
7958
|
+
code: import_types19.CATALOG_ERROR_CODES.BUNDLE_VERSION_REGRESSION,
|
|
7959
|
+
message: "This bundle version is regressive (feature removed / quota lowered / price raised). Publishing requires an explicit `forceRegressive: true` (UI confirmation dialog with MFA).",
|
|
7452
7960
|
changes: diff.changes
|
|
7453
7961
|
});
|
|
7454
7962
|
}
|
|
@@ -7487,8 +7995,8 @@ var BundlesService = class _BundlesService {
|
|
|
7487
7995
|
const hasHardBlockingWarning = blocking.some((w) => w.code === "BUNDLE_PLAN_KEY_UNKNOWN");
|
|
7488
7996
|
if (this.mode === "blocking" || hasHardBlockingWarning) {
|
|
7489
7997
|
throw new import_common43.UnprocessableEntityException({
|
|
7490
|
-
code:
|
|
7491
|
-
message: "
|
|
7998
|
+
code: import_types19.CATALOG_ERROR_CODES.STRICT_MODE_VIOLATIONS,
|
|
7999
|
+
message: "The strict-mode check found drift against the discovery snapshot.",
|
|
7492
8000
|
warnings
|
|
7493
8001
|
});
|
|
7494
8002
|
}
|
|
@@ -7501,8 +8009,8 @@ var BundlesService = class _BundlesService {
|
|
|
7501
8009
|
if (!validFromInput) {
|
|
7502
8010
|
if (existing.publishedAt !== null) {
|
|
7503
8011
|
throw new import_common43.UnprocessableEntityException({
|
|
7504
|
-
code:
|
|
7505
|
-
message: "Published
|
|
8012
|
+
code: import_types19.CATALOG_ERROR_CODES.BUNDLE_VERSION_VALID_FROM_REQUIRED,
|
|
8013
|
+
message: "Published bundle versions must keep a validFrom. Set a new future date, or edit the draft before publishing."
|
|
7506
8014
|
});
|
|
7507
8015
|
}
|
|
7508
8016
|
return;
|
|
@@ -7510,14 +8018,21 @@ var BundlesService = class _BundlesService {
|
|
|
7510
8018
|
const validFrom = this.parseRequiredBundleDate(validFromInput, "validFrom", "BUNDLE_VERSION_VALID_FROM_INVALID");
|
|
7511
8019
|
if (existing.publishedAt !== null && validFrom.getTime() <= Date.now()) {
|
|
7512
8020
|
throw new import_common43.UnprocessableEntityException({
|
|
7513
|
-
code:
|
|
7514
|
-
message: `validFrom (${validFrom.toISOString()})
|
|
8021
|
+
code: import_types19.CATALOG_ERROR_CODES.BUNDLE_VERSION_VALID_FROM_NOT_FUTURE,
|
|
8022
|
+
message: `validFrom (${validFrom.toISOString()}) must, for a published-but-future bundle version, still lie in the future.`,
|
|
8023
|
+
params: {
|
|
8024
|
+
validFrom: validFrom.toISOString()
|
|
8025
|
+
}
|
|
7515
8026
|
});
|
|
7516
8027
|
}
|
|
7517
8028
|
if (validUntil && validUntil <= validFrom) {
|
|
7518
8029
|
throw new import_common43.UnprocessableEntityException({
|
|
7519
|
-
code:
|
|
7520
|
-
message: `validUntil (${validUntil.toISOString()})
|
|
8030
|
+
code: import_types19.CATALOG_ERROR_CODES.BUNDLE_VERSION_VALID_UNTIL_BEFORE_FROM,
|
|
8031
|
+
message: `validUntil (${validUntil.toISOString()}) must be strictly after validFrom (${validFrom.toISOString()}).`,
|
|
8032
|
+
params: {
|
|
8033
|
+
validUntil: validUntil.toISOString(),
|
|
8034
|
+
validFrom: validFrom.toISOString()
|
|
8035
|
+
}
|
|
7521
8036
|
});
|
|
7522
8037
|
}
|
|
7523
8038
|
const previous = await this.findPreviousBundleVersion(existing);
|
|
@@ -7526,8 +8041,12 @@ var BundlesService = class _BundlesService {
|
|
|
7526
8041
|
if (Number.isNaN(previousValidFrom.getTime())) return;
|
|
7527
8042
|
if (validFrom <= previousValidFrom) {
|
|
7528
8043
|
throw new import_common43.UnprocessableEntityException({
|
|
7529
|
-
code:
|
|
7530
|
-
message: `validFrom (${validFrom.toISOString()})
|
|
8044
|
+
code: import_types19.CATALOG_ERROR_CODES.BUNDLE_VERSION_VALID_FROM_NOT_AFTER_PREVIOUS,
|
|
8045
|
+
message: `validFrom (${validFrom.toISOString()}) must be strictly after the validFrom of the previous version (${previous.validFrom}).`,
|
|
8046
|
+
params: {
|
|
8047
|
+
validFrom: validFrom.toISOString(),
|
|
8048
|
+
previousValidFrom: previous.validFrom
|
|
8049
|
+
}
|
|
7531
8050
|
});
|
|
7532
8051
|
}
|
|
7533
8052
|
if (!previous.validUntil) return;
|
|
@@ -7537,8 +8056,11 @@ var BundlesService = class _BundlesService {
|
|
|
7537
8056
|
const requiredStart = new Date(previousValidUntil.getTime() + dayMs);
|
|
7538
8057
|
if (validFrom.getTime() !== requiredStart.getTime()) {
|
|
7539
8058
|
throw new import_common43.UnprocessableEntityException({
|
|
7540
|
-
code:
|
|
7541
|
-
message: `
|
|
8059
|
+
code: import_types19.CATALOG_ERROR_CODES.BUNDLE_VERSION_VALID_FROM_NOT_GAPLESS,
|
|
8060
|
+
message: `The predecessor has validUntil=${previous.validUntil.slice(0, 10)} \u2014 the successor must start seamlessly on the next day (${requiredStart.toISOString().slice(0, 10)}). Received: ${validFrom.toISOString().slice(0, 10)}.`,
|
|
8061
|
+
params: {
|
|
8062
|
+
received: validFrom.toISOString().slice(0, 10)
|
|
8063
|
+
},
|
|
7542
8064
|
requiredValidFrom: requiredStart.toISOString().slice(0, 10),
|
|
7543
8065
|
previousValidUntil: previous.validUntil
|
|
7544
8066
|
});
|
|
@@ -7557,7 +8079,11 @@ var BundlesService = class _BundlesService {
|
|
|
7557
8079
|
if (Number.isNaN(date.getTime())) {
|
|
7558
8080
|
throw new import_common43.UnprocessableEntityException({
|
|
7559
8081
|
code,
|
|
7560
|
-
message: `${field} '${input}'
|
|
8082
|
+
message: `${field} '${input}' is not a valid date`,
|
|
8083
|
+
params: {
|
|
8084
|
+
field,
|
|
8085
|
+
value: input
|
|
8086
|
+
}
|
|
7561
8087
|
});
|
|
7562
8088
|
}
|
|
7563
8089
|
return date;
|
|
@@ -7592,6 +8118,7 @@ BundlesService = _ts_decorate44([
|
|
|
7592
8118
|
|
|
7593
8119
|
// src/catalog/marketing-projections.service.ts
|
|
7594
8120
|
var import_common44 = require("@nestjs/common");
|
|
8121
|
+
var import_types20 = require("@saasicat/types");
|
|
7595
8122
|
function _ts_decorate45(decorators, target, key, desc) {
|
|
7596
8123
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
7597
8124
|
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
@@ -7623,7 +8150,13 @@ var MarketingProjectionsService = class {
|
|
|
7623
8150
|
async getById(id) {
|
|
7624
8151
|
const row = await this.repo.findById(id);
|
|
7625
8152
|
if (!row) {
|
|
7626
|
-
throw new import_common44.NotFoundException(
|
|
8153
|
+
throw new import_common44.NotFoundException({
|
|
8154
|
+
code: import_types20.CATALOG_ERROR_CODES.MARKETING_PROJECTION_NOT_FOUND,
|
|
8155
|
+
message: `MarketingProjection '${id}' not found`,
|
|
8156
|
+
params: {
|
|
8157
|
+
projectionId: id
|
|
8158
|
+
}
|
|
8159
|
+
});
|
|
7627
8160
|
}
|
|
7628
8161
|
return row;
|
|
7629
8162
|
}
|
|
@@ -7631,7 +8164,15 @@ var MarketingProjectionsService = class {
|
|
|
7631
8164
|
const locale = data.locale ?? "de";
|
|
7632
8165
|
const existing = await this.repo.findByTarget(data.targetType, data.targetVersionId, locale);
|
|
7633
8166
|
if (existing) {
|
|
7634
|
-
throw new import_common44.ConflictException(
|
|
8167
|
+
throw new import_common44.ConflictException({
|
|
8168
|
+
code: import_types20.CATALOG_ERROR_CODES.MARKETING_PROJECTION_ALREADY_EXISTS,
|
|
8169
|
+
message: `Marketing projection for ${data.targetType}/${data.targetVersionId}/${locale} already exists \u2014 use PATCH to edit it`,
|
|
8170
|
+
params: {
|
|
8171
|
+
targetType: data.targetType,
|
|
8172
|
+
targetVersionId: data.targetVersionId,
|
|
8173
|
+
locale
|
|
8174
|
+
}
|
|
8175
|
+
});
|
|
7635
8176
|
}
|
|
7636
8177
|
return this.repo.create({
|
|
7637
8178
|
...data,
|
|
@@ -7641,14 +8182,26 @@ var MarketingProjectionsService = class {
|
|
|
7641
8182
|
async update(id, data) {
|
|
7642
8183
|
const existing = await this.repo.findById(id);
|
|
7643
8184
|
if (!existing) {
|
|
7644
|
-
throw new import_common44.NotFoundException(
|
|
8185
|
+
throw new import_common44.NotFoundException({
|
|
8186
|
+
code: import_types20.CATALOG_ERROR_CODES.MARKETING_PROJECTION_NOT_FOUND,
|
|
8187
|
+
message: `MarketingProjection '${id}' not found`,
|
|
8188
|
+
params: {
|
|
8189
|
+
projectionId: id
|
|
8190
|
+
}
|
|
8191
|
+
});
|
|
7645
8192
|
}
|
|
7646
8193
|
return this.repo.update(id, data);
|
|
7647
8194
|
}
|
|
7648
8195
|
async delete(id) {
|
|
7649
8196
|
const existing = await this.repo.findById(id);
|
|
7650
8197
|
if (!existing) {
|
|
7651
|
-
throw new import_common44.NotFoundException(
|
|
8198
|
+
throw new import_common44.NotFoundException({
|
|
8199
|
+
code: import_types20.CATALOG_ERROR_CODES.MARKETING_PROJECTION_NOT_FOUND,
|
|
8200
|
+
message: `MarketingProjection '${id}' not found`,
|
|
8201
|
+
params: {
|
|
8202
|
+
projectionId: id
|
|
8203
|
+
}
|
|
8204
|
+
});
|
|
7652
8205
|
}
|
|
7653
8206
|
await this.repo.delete(id);
|
|
7654
8207
|
}
|
|
@@ -7664,6 +8217,7 @@ MarketingProjectionsService = _ts_decorate45([
|
|
|
7664
8217
|
|
|
7665
8218
|
// src/catalog/catalog-entries.service.ts
|
|
7666
8219
|
var import_common45 = require("@nestjs/common");
|
|
8220
|
+
var import_types21 = require("@saasicat/types");
|
|
7667
8221
|
|
|
7668
8222
|
// src/catalog/approval-signature.ts
|
|
7669
8223
|
function featureApprovalSignature(featureKey, snapshot) {
|
|
@@ -7776,14 +8330,14 @@ var CatalogEntriesService = class _CatalogEntriesService {
|
|
|
7776
8330
|
if (!this.autoSyncAtBoot) return;
|
|
7777
8331
|
const snapshot = resolveDiscoverySnapshot(this.snapshot, this.scanner);
|
|
7778
8332
|
if (!snapshot) {
|
|
7779
|
-
this.logger.warn("Discovery-
|
|
8333
|
+
this.logger.warn("Discovery auto-sync skipped: no snapshot (neither token nor DiscoveryScanner injected). Wire up DiscoveryModule (#25).");
|
|
7780
8334
|
return;
|
|
7781
8335
|
}
|
|
7782
8336
|
try {
|
|
7783
8337
|
const result = await this.syncFromSnapshot(snapshot);
|
|
7784
8338
|
this.logger.log(`Discovery-Auto-Sync (${snapshot.app.key}): Features ${result.features.discovered}\u2191/${result.features.retired}\u2193/${result.features.outdated}\u26A0, Quotas ${result.quotas.discovered}\u2191/${result.quotas.retired}\u2193/${result.quotas.outdated}\u26A0, Capabilities ${result.capabilities.discovered}\u2191/${result.capabilities.retired}\u2193`);
|
|
7785
8339
|
} catch (err) {
|
|
7786
|
-
this.logger.error(`Discovery-
|
|
8340
|
+
this.logger.error(`Discovery auto-sync on boot failed (${snapshot.app.key}) \u2014 the catalog may be stale; re-sync manually via POST /admin/catalog/discovery/sync`, err instanceof Error ? err.stack : String(err));
|
|
7787
8341
|
}
|
|
7788
8342
|
}
|
|
7789
8343
|
listCapabilities(projectKey, codeStatus) {
|
|
@@ -7813,7 +8367,14 @@ var CatalogEntriesService = class _CatalogEntriesService {
|
|
|
7813
8367
|
async reviewFeature(projectKey, featureKey, data, reviewedBy) {
|
|
7814
8368
|
const existing = await this.repo.findFeature(projectKey, featureKey);
|
|
7815
8369
|
if (!existing) {
|
|
7816
|
-
throw new import_common45.NotFoundException(
|
|
8370
|
+
throw new import_common45.NotFoundException({
|
|
8371
|
+
code: import_types21.CATALOG_ERROR_CODES.FEATURE_NOT_FOUND,
|
|
8372
|
+
message: `Feature '${featureKey}' not found in project '${projectKey}'`,
|
|
8373
|
+
params: {
|
|
8374
|
+
featureKey,
|
|
8375
|
+
projectKey
|
|
8376
|
+
}
|
|
8377
|
+
});
|
|
7817
8378
|
}
|
|
7818
8379
|
return this.repo.setFeatureReview(projectKey, featureKey, this.resolveReviewUpdate(existing, data.discoveryStatus, reviewedBy, (snapshot) => featureApprovalSignature(featureKey, snapshot)));
|
|
7819
8380
|
}
|
|
@@ -7821,24 +8382,47 @@ var CatalogEntriesService = class _CatalogEntriesService {
|
|
|
7821
8382
|
async reviewQuota(projectKey, quotaKey, data, reviewedBy) {
|
|
7822
8383
|
const existing = await this.repo.findQuota(projectKey, quotaKey);
|
|
7823
8384
|
if (!existing) {
|
|
7824
|
-
throw new import_common45.NotFoundException(
|
|
8385
|
+
throw new import_common45.NotFoundException({
|
|
8386
|
+
code: import_types21.CATALOG_ERROR_CODES.QUOTA_NOT_FOUND,
|
|
8387
|
+
message: `Quota '${quotaKey}' not found in project '${projectKey}'`,
|
|
8388
|
+
params: {
|
|
8389
|
+
quotaKey,
|
|
8390
|
+
projectKey
|
|
8391
|
+
}
|
|
8392
|
+
});
|
|
7825
8393
|
}
|
|
7826
8394
|
return this.repo.setQuotaReview(projectKey, quotaKey, this.resolveReviewUpdate(existing, data.discoveryStatus, reviewedBy, (snapshot) => {
|
|
7827
8395
|
const discovered = snapshot.quotas.find((q) => q.quotaKey === quotaKey);
|
|
7828
8396
|
if (!discovered) {
|
|
7829
|
-
throw new import_common45.UnprocessableEntityException(
|
|
8397
|
+
throw new import_common45.UnprocessableEntityException({
|
|
8398
|
+
code: import_types21.CATALOG_ERROR_CODES.QUOTA_NOT_IN_DISCOVERY_SNAPSHOT,
|
|
8399
|
+
message: `Quota '${quotaKey}' is not in the discovery snapshot \u2014 a quota that is not declared in code cannot be approved`,
|
|
8400
|
+
params: {
|
|
8401
|
+
quotaKey
|
|
8402
|
+
}
|
|
8403
|
+
});
|
|
7830
8404
|
}
|
|
7831
8405
|
return quotaApprovalSignature(discovered);
|
|
7832
8406
|
}));
|
|
7833
8407
|
}
|
|
7834
8408
|
resolveReviewUpdate(existing, target, reviewedBy, signatureOf) {
|
|
7835
8409
|
if (!REVIEW_TRANSITIONS[existing.discoveryStatus].includes(target)) {
|
|
7836
|
-
throw new import_common45.UnprocessableEntityException(
|
|
8410
|
+
throw new import_common45.UnprocessableEntityException({
|
|
8411
|
+
code: import_types21.CATALOG_ERROR_CODES.DISCOVERY_STATUS_TRANSITION_INVALID,
|
|
8412
|
+
message: `Transition '${existing.discoveryStatus}' \u2192 '${target}' is not allowed`,
|
|
8413
|
+
params: {
|
|
8414
|
+
from: existing.discoveryStatus,
|
|
8415
|
+
to: target
|
|
8416
|
+
}
|
|
8417
|
+
});
|
|
7837
8418
|
}
|
|
7838
8419
|
if (target === "approved") {
|
|
7839
8420
|
const snapshot = resolveDiscoverySnapshot(this.snapshot, this.scanner);
|
|
7840
8421
|
if (!snapshot) {
|
|
7841
|
-
throw new import_common45.UnprocessableEntityException(
|
|
8422
|
+
throw new import_common45.UnprocessableEntityException({
|
|
8423
|
+
code: import_types21.CATALOG_ERROR_CODES.DISCOVERY_NOT_INITIALIZED,
|
|
8424
|
+
message: "Approval requires a discovery snapshot \u2014 discovery is not initialised (#25)"
|
|
8425
|
+
});
|
|
7842
8426
|
}
|
|
7843
8427
|
return {
|
|
7844
8428
|
discoveryStatus: "approved",
|
|
@@ -8031,7 +8615,7 @@ var CatalogEntriesService = class _CatalogEntriesService {
|
|
|
8031
8615
|
const claimants = claimantsByOldKey.get(entry.key);
|
|
8032
8616
|
if (claimants && claimants.length > 0) {
|
|
8033
8617
|
if (claimants.length > 1) {
|
|
8034
|
-
this.logger.warn(`${type} '${entry.key}'
|
|
8618
|
+
this.logger.warn(`${type} '${entry.key}' is claimed by several replaces declarations (${claimants.join(", ")}); '${claimants[0]}' wins \u2014 clean up the replaces in the code (#39)`);
|
|
8035
8619
|
}
|
|
8036
8620
|
target = claimants[0];
|
|
8037
8621
|
}
|
|
@@ -8044,7 +8628,7 @@ var CatalogEntriesService = class _CatalogEntriesService {
|
|
|
8044
8628
|
if (changes.length === 0) return 0;
|
|
8045
8629
|
const setSuccessor = type === "feature" ? this.repo.setFeatureSuccessor?.bind(this.repo) : this.repo.setQuotaSuccessor?.bind(this.repo);
|
|
8046
8630
|
if (!setSuccessor) {
|
|
8047
|
-
this.logger.warn(`CatalogEntryRepository
|
|
8631
|
+
this.logger.warn(`CatalogEntryRepository does not implement set${type === "feature" ? "Feature" : "Quota"}Successor \u2014 ${changes.length} successor pointers (#39) are not persisted`);
|
|
8048
8632
|
return 0;
|
|
8049
8633
|
}
|
|
8050
8634
|
let replaced = 0;
|
|
@@ -8212,7 +8796,7 @@ var ListCatalogEntriesQueryDto = class {
|
|
|
8212
8796
|
_ts_decorate48([
|
|
8213
8797
|
(0, import_class_validator6.IsString)(),
|
|
8214
8798
|
(0, import_class_validator6.Matches)(PROJECT_KEY_PATTERN, {
|
|
8215
|
-
message: "projectKey
|
|
8799
|
+
message: "projectKey must be kebab-case"
|
|
8216
8800
|
}),
|
|
8217
8801
|
_ts_metadata35("design:type", String)
|
|
8218
8802
|
], ListCatalogEntriesQueryDto.prototype, "projectKey", void 0);
|
|
@@ -8237,7 +8821,7 @@ var ReviewCatalogEntryDto = class {
|
|
|
8237
8821
|
_ts_decorate48([
|
|
8238
8822
|
(0, import_class_validator6.IsString)(),
|
|
8239
8823
|
(0, import_class_validator6.IsIn)(REVIEW_STATUSES, {
|
|
8240
|
-
message: "discoveryStatus
|
|
8824
|
+
message: "discoveryStatus must be pending|approved|outdated|obsolete"
|
|
8241
8825
|
}),
|
|
8242
8826
|
_ts_metadata35("design:type", typeof DiscoveryStatus === "undefined" ? Object : DiscoveryStatus)
|
|
8243
8827
|
], ReviewCatalogEntryDto.prototype, "discoveryStatus", void 0);
|
|
@@ -8538,6 +9122,7 @@ __name(buildCatalogEntriesController, "buildCatalogEntriesController");
|
|
|
8538
9122
|
|
|
8539
9123
|
// src/catalog/promotions.service.ts
|
|
8540
9124
|
var import_common48 = require("@nestjs/common");
|
|
9125
|
+
var import_types22 = require("@saasicat/types");
|
|
8541
9126
|
function _ts_decorate50(decorators, target, key, desc) {
|
|
8542
9127
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
8543
9128
|
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
@@ -8571,7 +9156,13 @@ var PromotionsService = class {
|
|
|
8571
9156
|
async getById(id) {
|
|
8572
9157
|
const row = await this.repo.findById(id);
|
|
8573
9158
|
if (!row) {
|
|
8574
|
-
throw new import_common48.NotFoundException(
|
|
9159
|
+
throw new import_common48.NotFoundException({
|
|
9160
|
+
code: import_types22.CATALOG_ERROR_CODES.PROMOTION_NOT_FOUND,
|
|
9161
|
+
message: `Promotion '${id}' not found`,
|
|
9162
|
+
params: {
|
|
9163
|
+
promotionId: id
|
|
9164
|
+
}
|
|
9165
|
+
});
|
|
8575
9166
|
}
|
|
8576
9167
|
return row;
|
|
8577
9168
|
}
|
|
@@ -8581,14 +9172,26 @@ var PromotionsService = class {
|
|
|
8581
9172
|
async update(id, data) {
|
|
8582
9173
|
const existing = await this.repo.findById(id);
|
|
8583
9174
|
if (!existing) {
|
|
8584
|
-
throw new import_common48.NotFoundException(
|
|
9175
|
+
throw new import_common48.NotFoundException({
|
|
9176
|
+
code: import_types22.CATALOG_ERROR_CODES.PROMOTION_NOT_FOUND,
|
|
9177
|
+
message: `Promotion '${id}' not found`,
|
|
9178
|
+
params: {
|
|
9179
|
+
promotionId: id
|
|
9180
|
+
}
|
|
9181
|
+
});
|
|
8585
9182
|
}
|
|
8586
9183
|
return this.repo.update(id, data);
|
|
8587
9184
|
}
|
|
8588
9185
|
async delete(id) {
|
|
8589
9186
|
const existing = await this.repo.findById(id);
|
|
8590
9187
|
if (!existing) {
|
|
8591
|
-
throw new import_common48.NotFoundException(
|
|
9188
|
+
throw new import_common48.NotFoundException({
|
|
9189
|
+
code: import_types22.CATALOG_ERROR_CODES.PROMOTION_NOT_FOUND,
|
|
9190
|
+
message: `Promotion '${id}' not found`,
|
|
9191
|
+
params: {
|
|
9192
|
+
promotionId: id
|
|
9193
|
+
}
|
|
9194
|
+
});
|
|
8592
9195
|
}
|
|
8593
9196
|
await this.repo.delete(id);
|
|
8594
9197
|
}
|
|
@@ -8645,7 +9248,7 @@ var ListPromotionsQueryDto = class {
|
|
|
8645
9248
|
_ts_decorate51([
|
|
8646
9249
|
(0, import_class_validator7.IsString)(),
|
|
8647
9250
|
(0, import_class_validator7.Matches)(PROJECT_KEY_PATTERN2, {
|
|
8648
|
-
message: "projectKey
|
|
9251
|
+
message: "projectKey must be kebab-case"
|
|
8649
9252
|
}),
|
|
8650
9253
|
_ts_metadata38("design:type", String)
|
|
8651
9254
|
], ListPromotionsQueryDto.prototype, "projectKey", void 0);
|
|
@@ -8713,14 +9316,14 @@ _ts_decorate51([
|
|
|
8713
9316
|
_ts_decorate51([
|
|
8714
9317
|
(0, import_class_validator7.IsString)(),
|
|
8715
9318
|
(0, import_class_validator7.Matches)(ISO_DATE_PATTERN2, {
|
|
8716
|
-
message: "validFrom
|
|
9319
|
+
message: "validFrom must be an ISO date"
|
|
8717
9320
|
}),
|
|
8718
9321
|
_ts_metadata38("design:type", String)
|
|
8719
9322
|
], CreatePromotionDto.prototype, "validFrom", void 0);
|
|
8720
9323
|
_ts_decorate51([
|
|
8721
9324
|
(0, import_class_validator7.IsString)(),
|
|
8722
9325
|
(0, import_class_validator7.Matches)(ISO_DATE_PATTERN2, {
|
|
8723
|
-
message: "validTo
|
|
9326
|
+
message: "validTo must be an ISO date"
|
|
8724
9327
|
}),
|
|
8725
9328
|
_ts_metadata38("design:type", String)
|
|
8726
9329
|
], CreatePromotionDto.prototype, "validTo", void 0);
|
|
@@ -9041,7 +9644,7 @@ var ListMarketingSettingsQueryDto = class {
|
|
|
9041
9644
|
_ts_decorate54([
|
|
9042
9645
|
(0, import_class_validator8.IsString)(),
|
|
9043
9646
|
(0, import_class_validator8.Matches)(PROJECT_KEY_PATTERN3, {
|
|
9044
|
-
message: "projectKey
|
|
9647
|
+
message: "projectKey must be kebab-case"
|
|
9045
9648
|
}),
|
|
9046
9649
|
_ts_metadata41("design:type", String)
|
|
9047
9650
|
], ListMarketingSettingsQueryDto.prototype, "projectKey", void 0);
|
|
@@ -9055,7 +9658,7 @@ var UpdateMarketingSettingsDto = class {
|
|
|
9055
9658
|
_ts_decorate54([
|
|
9056
9659
|
(0, import_class_validator8.IsString)(),
|
|
9057
9660
|
(0, import_class_validator8.Matches)(PROJECT_KEY_PATTERN3, {
|
|
9058
|
-
message: "projectKey
|
|
9661
|
+
message: "projectKey must be kebab-case"
|
|
9059
9662
|
}),
|
|
9060
9663
|
_ts_metadata41("design:type", String)
|
|
9061
9664
|
], UpdateMarketingSettingsDto.prototype, "projectKey", void 0);
|
|
@@ -9067,7 +9670,7 @@ _ts_decorate54([
|
|
|
9067
9670
|
}),
|
|
9068
9671
|
(0, import_class_validator8.Matches)(LOCALE_PATTERN, {
|
|
9069
9672
|
each: true,
|
|
9070
|
-
message: "Locale
|
|
9673
|
+
message: "Locale must be ISO-639-1"
|
|
9071
9674
|
}),
|
|
9072
9675
|
_ts_metadata41("design:type", Array)
|
|
9073
9676
|
], UpdateMarketingSettingsDto.prototype, "activeLocales", void 0);
|
|
@@ -9142,7 +9745,7 @@ __name(buildMarketingSettingsController, "buildMarketingSettingsController");
|
|
|
9142
9745
|
|
|
9143
9746
|
// src/catalog/public-marketing-catalog.service.ts
|
|
9144
9747
|
var import_common52 = require("@nestjs/common");
|
|
9145
|
-
var
|
|
9748
|
+
var import_types23 = require("@saasicat/types");
|
|
9146
9749
|
function _ts_decorate56(decorators, target, key, desc) {
|
|
9147
9750
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
9148
9751
|
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
@@ -9245,7 +9848,7 @@ var PublicMarketingCatalogService = class {
|
|
|
9245
9848
|
});
|
|
9246
9849
|
}
|
|
9247
9850
|
out.sort((a, b) => b.priority - a.priority);
|
|
9248
|
-
const requiresIndex = (0,
|
|
9851
|
+
const requiresIndex = (0, import_types23.buildFeatureRequiresIndex)([
|
|
9249
9852
|
...labelMeta.features
|
|
9250
9853
|
].map(([featureKey, meta]) => ({
|
|
9251
9854
|
featureKey,
|
|
@@ -9299,7 +9902,7 @@ var PublicMarketingCatalogService = class {
|
|
|
9299
9902
|
const features = [
|
|
9300
9903
|
...bv.features ?? []
|
|
9301
9904
|
];
|
|
9302
|
-
const requiresFeatures = (0,
|
|
9905
|
+
const requiresFeatures = (0, import_types23.collectUnsatisfiedRequires)(features, requiresIndex);
|
|
9303
9906
|
return {
|
|
9304
9907
|
bundleKey: bv.bundleKey,
|
|
9305
9908
|
label: marketing?.displayLabel || bv.label,
|
|
@@ -9443,12 +10046,12 @@ var PublicMarketingCatalogService = class {
|
|
|
9443
10046
|
};
|
|
9444
10047
|
}
|
|
9445
10048
|
resolvePromo(promotions, targetKey, targetType, locale, monthlyNet, yearlyNet, asOf) {
|
|
9446
|
-
const monthlyPromo = (0,
|
|
9447
|
-
const yearlyPromo = (0,
|
|
10049
|
+
const monthlyPromo = (0, import_types23.pickActivePromo)(promotions, targetKey, locale, "monthly", asOf, targetType);
|
|
10050
|
+
const yearlyPromo = (0, import_types23.pickActivePromo)(promotions, targetKey, locale, "yearly", asOf, targetType);
|
|
9448
10051
|
const promo = monthlyPromo ?? yearlyPromo;
|
|
9449
10052
|
if (!promo) return null;
|
|
9450
|
-
const monthlyResult = (0,
|
|
9451
|
-
const yearlyResult = (0,
|
|
10053
|
+
const monthlyResult = (0, import_types23.applyPromo)(monthlyPromo, monthlyNet);
|
|
10054
|
+
const yearlyResult = (0, import_types23.applyPromo)(yearlyPromo, yearlyNet);
|
|
9452
10055
|
const i18n = promo.i18n?.[locale] ?? promo.i18n?.[DEFAULT_LOCALE] ?? {};
|
|
9453
10056
|
return {
|
|
9454
10057
|
type: promo.type,
|
|
@@ -9555,6 +10158,7 @@ __name(parseAsOf, "parseAsOf");
|
|
|
9555
10158
|
|
|
9556
10159
|
// src/catalog/plans.service.ts
|
|
9557
10160
|
var import_common54 = require("@nestjs/common");
|
|
10161
|
+
var import_types24 = require("@saasicat/types");
|
|
9558
10162
|
function _ts_decorate58(decorators, target, key, desc) {
|
|
9559
10163
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
9560
10164
|
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
@@ -9601,24 +10205,55 @@ var PlansService = class {
|
|
|
9601
10205
|
}
|
|
9602
10206
|
async getPlan(planId) {
|
|
9603
10207
|
const plan = await this.repo.findById(planId);
|
|
9604
|
-
if (!plan)
|
|
10208
|
+
if (!plan) {
|
|
10209
|
+
throw new import_common54.NotFoundException({
|
|
10210
|
+
code: import_types24.CATALOG_ERROR_CODES.PLAN_NOT_FOUND,
|
|
10211
|
+
message: `Plan '${planId}' not found`,
|
|
10212
|
+
params: {
|
|
10213
|
+
planId
|
|
10214
|
+
}
|
|
10215
|
+
});
|
|
10216
|
+
}
|
|
9605
10217
|
return plan;
|
|
9606
10218
|
}
|
|
9607
10219
|
async createPlan(data) {
|
|
9608
10220
|
const existing = await this.repo.findByKey(data.projectKey, data.planKey);
|
|
9609
10221
|
if (existing) {
|
|
9610
|
-
throw new import_common54.UnprocessableEntityException(
|
|
10222
|
+
throw new import_common54.UnprocessableEntityException({
|
|
10223
|
+
code: import_types24.CATALOG_ERROR_CODES.PLAN_ALREADY_EXISTS,
|
|
10224
|
+
message: `Plan '${data.planKey}' already exists in project '${data.projectKey}'`,
|
|
10225
|
+
params: {
|
|
10226
|
+
planKey: data.planKey,
|
|
10227
|
+
projectKey: data.projectKey
|
|
10228
|
+
}
|
|
10229
|
+
});
|
|
9611
10230
|
}
|
|
9612
10231
|
return this.repo.create(data);
|
|
9613
10232
|
}
|
|
9614
10233
|
async updatePlan(planId, data) {
|
|
9615
10234
|
const existing = await this.repo.findById(planId);
|
|
9616
|
-
if (!existing)
|
|
10235
|
+
if (!existing) {
|
|
10236
|
+
throw new import_common54.NotFoundException({
|
|
10237
|
+
code: import_types24.CATALOG_ERROR_CODES.PLAN_NOT_FOUND,
|
|
10238
|
+
message: `Plan '${planId}' not found`,
|
|
10239
|
+
params: {
|
|
10240
|
+
planId
|
|
10241
|
+
}
|
|
10242
|
+
});
|
|
10243
|
+
}
|
|
9617
10244
|
return this.repo.update(planId, data);
|
|
9618
10245
|
}
|
|
9619
10246
|
async softDeletePlan(planId) {
|
|
9620
10247
|
const existing = await this.repo.findById(planId);
|
|
9621
|
-
if (!existing)
|
|
10248
|
+
if (!existing) {
|
|
10249
|
+
throw new import_common54.NotFoundException({
|
|
10250
|
+
code: import_types24.CATALOG_ERROR_CODES.PLAN_NOT_FOUND,
|
|
10251
|
+
message: `Plan '${planId}' not found`,
|
|
10252
|
+
params: {
|
|
10253
|
+
planId
|
|
10254
|
+
}
|
|
10255
|
+
});
|
|
10256
|
+
}
|
|
9622
10257
|
if (existing.deletedAt !== null) return;
|
|
9623
10258
|
await this.assertNoPublishedVersions(existing.planKey, "soft-delete");
|
|
9624
10259
|
await this.repo.softDelete(planId);
|
|
@@ -9635,13 +10270,21 @@ var PlansService = class {
|
|
|
9635
10270
|
*/
|
|
9636
10271
|
async hardDeletePlan(planId) {
|
|
9637
10272
|
const existing = await this.repo.findById(planId);
|
|
9638
|
-
if (!existing)
|
|
10273
|
+
if (!existing) {
|
|
10274
|
+
throw new import_common54.NotFoundException({
|
|
10275
|
+
code: import_types24.CATALOG_ERROR_CODES.PLAN_NOT_FOUND,
|
|
10276
|
+
message: `Plan '${planId}' not found`,
|
|
10277
|
+
params: {
|
|
10278
|
+
planId
|
|
10279
|
+
}
|
|
10280
|
+
});
|
|
10281
|
+
}
|
|
9639
10282
|
await this.assertNoPublishedVersions(existing.planKey, "hard-delete");
|
|
9640
10283
|
await this.assertNoDrafts(existing.planKey);
|
|
9641
10284
|
if (typeof this.repo.hardDelete !== "function") {
|
|
9642
10285
|
throw new import_common54.UnprocessableEntityException({
|
|
9643
|
-
code:
|
|
9644
|
-
message: "
|
|
10286
|
+
code: import_types24.CATALOG_ERROR_CODES.PLAN_HARD_DELETE_NOT_IMPLEMENTED,
|
|
10287
|
+
message: "Hard delete is not implemented in the current repository. Implement PlanRepository.hardDelete."
|
|
9645
10288
|
});
|
|
9646
10289
|
}
|
|
9647
10290
|
await this.repo.hardDelete(planId);
|
|
@@ -9661,8 +10304,12 @@ var PlansService = class {
|
|
|
9661
10304
|
const live = published.filter((v) => v.supersededAt === null).length;
|
|
9662
10305
|
const superseded = published.length - live;
|
|
9663
10306
|
throw new import_common54.UnprocessableEntityException({
|
|
9664
|
-
code:
|
|
9665
|
-
message: `Plan '${planKey}'
|
|
10307
|
+
code: import_types24.CATALOG_ERROR_CODES.PLAN_HAS_PUBLISHED_VERSIONS,
|
|
10308
|
+
message: `Plan '${planKey}' cannot be ${op === "hard-delete" ? "deleted" : "archived"} \u2014 it has ${published.length} published version(s) (${live} live, ${superseded} superseded). Existing subscriptions reference those versions (contract protection P1), so the plan record must be preserved.`,
|
|
10309
|
+
params: {
|
|
10310
|
+
planKey,
|
|
10311
|
+
operation: op
|
|
10312
|
+
},
|
|
9666
10313
|
publishedCount: published.length,
|
|
9667
10314
|
liveCount: live,
|
|
9668
10315
|
supersededCount: superseded
|
|
@@ -9674,8 +10321,11 @@ var PlansService = class {
|
|
|
9674
10321
|
const drafts = versions.filter((v) => v.publishedAt === null);
|
|
9675
10322
|
if (drafts.length === 0) return;
|
|
9676
10323
|
throw new import_common54.UnprocessableEntityException({
|
|
9677
|
-
code:
|
|
9678
|
-
message: `Plan '${planKey}'
|
|
10324
|
+
code: import_types24.CATALOG_ERROR_CODES.PLAN_HAS_DRAFTS,
|
|
10325
|
+
message: `Plan '${planKey}' still has ${drafts.length} open draft version(s). Discard them first (DELETE /admin/catalog/plan-versions/:id) or publish them.`,
|
|
10326
|
+
params: {
|
|
10327
|
+
planKey
|
|
10328
|
+
},
|
|
9679
10329
|
draftCount: drafts.length
|
|
9680
10330
|
});
|
|
9681
10331
|
}
|
|
@@ -9694,7 +10344,7 @@ PlansService = _ts_decorate58([
|
|
|
9694
10344
|
|
|
9695
10345
|
// src/catalog/plan-versions.service.ts
|
|
9696
10346
|
var import_common55 = require("@nestjs/common");
|
|
9697
|
-
var
|
|
10347
|
+
var import_types25 = require("@saasicat/types");
|
|
9698
10348
|
function _ts_decorate59(decorators, target, key, desc) {
|
|
9699
10349
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
9700
10350
|
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
@@ -9733,7 +10383,7 @@ var PlanVersionsService = class _PlanVersionsService {
|
|
|
9733
10383
|
this.mode = config.strictModeCheckMode ?? "blocking";
|
|
9734
10384
|
this.marketedOnly = new Set(config.marketedOnlyFeatures ?? []);
|
|
9735
10385
|
if (this.mode === "blocking" && !hasDiscoverySnapshotSource(this.snapshot, this.scanner)) {
|
|
9736
|
-
this.logger.error("PlanVersionsService: strictModeCheckMode=blocking
|
|
10386
|
+
this.logger.error("PlanVersionsService: strictModeCheckMode=blocking without a DiscoverySnapshot source \u2014 degrading to warn-only. Wire DiscoveryModule (#25).");
|
|
9737
10387
|
this.mode = "warn-only";
|
|
9738
10388
|
}
|
|
9739
10389
|
const required = [
|
|
@@ -9747,7 +10397,7 @@ var PlanVersionsService = class _PlanVersionsService {
|
|
|
9747
10397
|
];
|
|
9748
10398
|
for (const key of required) {
|
|
9749
10399
|
if (typeof this.repo[key] !== "function") {
|
|
9750
|
-
throw new Error(`PlanVersionsService: PlanRepository.${String(key)}
|
|
10400
|
+
throw new Error(`PlanVersionsService: PlanRepository.${String(key)} is missing \u2014 the repository does not implement the lifecycle methods.`);
|
|
9751
10401
|
}
|
|
9752
10402
|
}
|
|
9753
10403
|
}
|
|
@@ -9763,18 +10413,27 @@ var PlanVersionsService = class _PlanVersionsService {
|
|
|
9763
10413
|
async discardPlanDraft(versionId) {
|
|
9764
10414
|
const existing = await this.repo.findVersionById(versionId);
|
|
9765
10415
|
if (!existing) {
|
|
9766
|
-
throw new import_common55.NotFoundException(
|
|
10416
|
+
throw new import_common55.NotFoundException({
|
|
10417
|
+
code: import_types25.CATALOG_ERROR_CODES.PLAN_VERSION_NOT_FOUND,
|
|
10418
|
+
message: `PlanVersion '${versionId}' not found`,
|
|
10419
|
+
params: {
|
|
10420
|
+
versionId
|
|
10421
|
+
}
|
|
10422
|
+
});
|
|
9767
10423
|
}
|
|
9768
10424
|
if (existing.publishedAt !== null) {
|
|
9769
10425
|
throw new import_common55.UnprocessableEntityException({
|
|
9770
|
-
code:
|
|
9771
|
-
message: `PlanVersion '${versionId}'
|
|
10426
|
+
code: import_types25.CATALOG_ERROR_CODES.PLAN_VERSION_ALREADY_PUBLISHED,
|
|
10427
|
+
message: `PlanVersion '${versionId}' is already published and cannot be discarded.`,
|
|
10428
|
+
params: {
|
|
10429
|
+
versionId
|
|
10430
|
+
}
|
|
9772
10431
|
});
|
|
9773
10432
|
}
|
|
9774
10433
|
if (typeof this.repo.deletePlanVersionDraft !== "function") {
|
|
9775
10434
|
throw new import_common55.UnprocessableEntityException({
|
|
9776
|
-
code:
|
|
9777
|
-
message: "Discard
|
|
10435
|
+
code: import_types25.CATALOG_ERROR_CODES.PLAN_VERSION_DISCARD_NOT_IMPLEMENTED,
|
|
10436
|
+
message: "Discard is not implemented in the current repository. Implement PlanRepository.deletePlanVersionDraft."
|
|
9778
10437
|
});
|
|
9779
10438
|
}
|
|
9780
10439
|
await this.repo.deletePlanVersionDraft(versionId);
|
|
@@ -9787,7 +10446,13 @@ var PlanVersionsService = class _PlanVersionsService {
|
|
|
9787
10446
|
async listPlanVersions(planUuid) {
|
|
9788
10447
|
const plan = await this.repo.findById(planUuid);
|
|
9789
10448
|
if (!plan) {
|
|
9790
|
-
throw new import_common55.NotFoundException(
|
|
10449
|
+
throw new import_common55.NotFoundException({
|
|
10450
|
+
code: import_types25.CATALOG_ERROR_CODES.PLAN_NOT_FOUND,
|
|
10451
|
+
message: `Plan '${planUuid}' not found`,
|
|
10452
|
+
params: {
|
|
10453
|
+
planId: planUuid
|
|
10454
|
+
}
|
|
10455
|
+
});
|
|
9791
10456
|
}
|
|
9792
10457
|
const versions = await this.repo.listVersions(plan.planKey);
|
|
9793
10458
|
return this.annotateEditability(versions);
|
|
@@ -9795,7 +10460,13 @@ var PlanVersionsService = class _PlanVersionsService {
|
|
|
9795
10460
|
async getPlanVersion(versionId) {
|
|
9796
10461
|
const version = await this.repo.findVersionById(versionId);
|
|
9797
10462
|
if (!version) {
|
|
9798
|
-
throw new import_common55.NotFoundException(
|
|
10463
|
+
throw new import_common55.NotFoundException({
|
|
10464
|
+
code: import_types25.CATALOG_ERROR_CODES.PLAN_VERSION_NOT_FOUND,
|
|
10465
|
+
message: `PlanVersion '${versionId}' not found`,
|
|
10466
|
+
params: {
|
|
10467
|
+
versionId
|
|
10468
|
+
}
|
|
10469
|
+
});
|
|
9799
10470
|
}
|
|
9800
10471
|
const [annotated] = await this.annotateEditability([
|
|
9801
10472
|
version
|
|
@@ -9833,12 +10504,25 @@ var PlanVersionsService = class _PlanVersionsService {
|
|
|
9833
10504
|
async createPlanDraft(data) {
|
|
9834
10505
|
const plan = await this.repo.findById(data.planId);
|
|
9835
10506
|
if (!plan) {
|
|
9836
|
-
throw new import_common55.NotFoundException(
|
|
10507
|
+
throw new import_common55.NotFoundException({
|
|
10508
|
+
code: import_types25.CATALOG_ERROR_CODES.PLAN_NOT_FOUND,
|
|
10509
|
+
message: `Plan '${data.planId}' not found`,
|
|
10510
|
+
params: {
|
|
10511
|
+
planId: data.planId
|
|
10512
|
+
}
|
|
10513
|
+
});
|
|
9837
10514
|
}
|
|
9838
10515
|
const planKey = plan.planKey;
|
|
9839
10516
|
const existingDraft = await this.repo.findCurrentDraft(planKey);
|
|
9840
10517
|
if (existingDraft) {
|
|
9841
|
-
throw new import_common55.UnprocessableEntityException(
|
|
10518
|
+
throw new import_common55.UnprocessableEntityException({
|
|
10519
|
+
code: import_types25.CATALOG_ERROR_CODES.PLAN_DRAFT_ALREADY_EXISTS,
|
|
10520
|
+
message: `Plan '${planKey}' already has a draft version v${existingDraft.version}; publish or discard it first`,
|
|
10521
|
+
params: {
|
|
10522
|
+
planKey,
|
|
10523
|
+
draftVersion: existingDraft.version
|
|
10524
|
+
}
|
|
10525
|
+
});
|
|
9842
10526
|
}
|
|
9843
10527
|
let baseVersionId = data.baseVersionId;
|
|
9844
10528
|
if (baseVersionId === void 0) {
|
|
@@ -9863,16 +10547,25 @@ var PlanVersionsService = class _PlanVersionsService {
|
|
|
9863
10547
|
async updatePlanDraft(versionId, data) {
|
|
9864
10548
|
const existing = await this.repo.findVersionById(versionId);
|
|
9865
10549
|
if (!existing) {
|
|
9866
|
-
throw new import_common55.NotFoundException(
|
|
10550
|
+
throw new import_common55.NotFoundException({
|
|
10551
|
+
code: import_types25.CATALOG_ERROR_CODES.PLAN_VERSION_NOT_FOUND,
|
|
10552
|
+
message: `PlanVersion '${versionId}' not found`,
|
|
10553
|
+
params: {
|
|
10554
|
+
versionId
|
|
10555
|
+
}
|
|
10556
|
+
});
|
|
9867
10557
|
}
|
|
9868
10558
|
const [annotated] = await this.annotateEditability([
|
|
9869
10559
|
existing
|
|
9870
10560
|
]);
|
|
9871
|
-
const { editable } = (0,
|
|
10561
|
+
const { editable } = (0, import_types25.isVersionEditable)(annotated);
|
|
9872
10562
|
if (!editable) {
|
|
9873
10563
|
throw new import_common55.UnprocessableEntityException({
|
|
9874
|
-
code:
|
|
9875
|
-
message: `PlanVersion '${versionId}'
|
|
10564
|
+
code: import_types25.CATALOG_ERROR_CODES.PLAN_VERSION_NOT_EDITABLE,
|
|
10565
|
+
message: `PlanVersion '${versionId}' is not editable. Only drafts and published versions are editable that are latest-in-chain, bind no subscription yet, and whose validFrom lies in the future.`,
|
|
10566
|
+
params: {
|
|
10567
|
+
versionId
|
|
10568
|
+
}
|
|
9876
10569
|
});
|
|
9877
10570
|
}
|
|
9878
10571
|
const merged = {
|
|
@@ -9890,10 +10583,22 @@ var PlanVersionsService = class _PlanVersionsService {
|
|
|
9890
10583
|
async publishPlanVersion(versionId, publishMeta) {
|
|
9891
10584
|
const draft = await this.repo.findVersionById(versionId);
|
|
9892
10585
|
if (!draft) {
|
|
9893
|
-
throw new import_common55.NotFoundException(
|
|
10586
|
+
throw new import_common55.NotFoundException({
|
|
10587
|
+
code: import_types25.CATALOG_ERROR_CODES.PLAN_VERSION_NOT_FOUND,
|
|
10588
|
+
message: `PlanVersion '${versionId}' not found`,
|
|
10589
|
+
params: {
|
|
10590
|
+
versionId
|
|
10591
|
+
}
|
|
10592
|
+
});
|
|
9894
10593
|
}
|
|
9895
10594
|
if (draft.publishedAt !== null) {
|
|
9896
|
-
throw new import_common55.UnprocessableEntityException(
|
|
10595
|
+
throw new import_common55.UnprocessableEntityException({
|
|
10596
|
+
code: import_types25.CATALOG_ERROR_CODES.PLAN_VERSION_ALREADY_PUBLISHED,
|
|
10597
|
+
message: `PlanVersion '${versionId}' is already published`,
|
|
10598
|
+
params: {
|
|
10599
|
+
versionId
|
|
10600
|
+
}
|
|
10601
|
+
});
|
|
9897
10602
|
}
|
|
9898
10603
|
const warnings = await this.runStrictCheck({
|
|
9899
10604
|
features: draft.features,
|
|
@@ -9905,8 +10610,8 @@ var PlanVersionsService = class _PlanVersionsService {
|
|
|
9905
10610
|
const yearly = Number.parseFloat(String(draft.yearlyNet ?? "0"));
|
|
9906
10611
|
if (monthly <= 0 || yearly <= 0) {
|
|
9907
10612
|
throw new import_common55.UnprocessableEntityException({
|
|
9908
|
-
code:
|
|
9909
|
-
message: "
|
|
10613
|
+
code: import_types25.CATALOG_ERROR_CODES.PLAN_VERSION_ZERO_PRICE,
|
|
10614
|
+
message: "A plan version cannot be published with a price of 0.00 (guard against seed placeholder). For deliberately free special contracts, set allowZeroPrice.",
|
|
9910
10615
|
monthlyNet: String(draft.monthlyNet),
|
|
9911
10616
|
yearlyNet: String(draft.yearlyNet)
|
|
9912
10617
|
});
|
|
@@ -9916,23 +10621,30 @@ var PlanVersionsService = class _PlanVersionsService {
|
|
|
9916
10621
|
const validFromInput = publishMeta.validFrom ?? draft.validFrom;
|
|
9917
10622
|
if (!validFromInput) {
|
|
9918
10623
|
throw new import_common55.UnprocessableEntityException({
|
|
9919
|
-
code:
|
|
9920
|
-
message: "
|
|
10624
|
+
code: import_types25.CATALOG_ERROR_CODES.PLAN_VERSION_VALID_FROM_REQUIRED,
|
|
10625
|
+
message: "validFrom must be set when publishing (on the draft or the publish call). SPEC_V2 \xA74.2."
|
|
9921
10626
|
});
|
|
9922
10627
|
}
|
|
9923
10628
|
const validFrom = new Date(validFromInput);
|
|
9924
10629
|
if (Number.isNaN(validFrom.getTime())) {
|
|
9925
10630
|
throw new import_common55.UnprocessableEntityException({
|
|
9926
|
-
code:
|
|
9927
|
-
message: `validFrom '${validFromInput}'
|
|
10631
|
+
code: import_types25.CATALOG_ERROR_CODES.PLAN_VERSION_VALID_FROM_INVALID,
|
|
10632
|
+
message: `validFrom '${validFromInput}' is not a valid date`,
|
|
10633
|
+
params: {
|
|
10634
|
+
validFrom: validFromInput
|
|
10635
|
+
}
|
|
9928
10636
|
});
|
|
9929
10637
|
}
|
|
9930
10638
|
if (previous?.validFrom) {
|
|
9931
10639
|
const prevFrom = new Date(previous.validFrom);
|
|
9932
10640
|
if (validFrom <= prevFrom) {
|
|
9933
10641
|
throw new import_common55.UnprocessableEntityException({
|
|
9934
|
-
code:
|
|
9935
|
-
message: `validFrom (${validFrom.toISOString()})
|
|
10642
|
+
code: import_types25.CATALOG_ERROR_CODES.PLAN_VERSION_VALID_FROM_NOT_AFTER_PREVIOUS,
|
|
10643
|
+
message: `validFrom (${validFrom.toISOString()}) must be strictly after the validFrom of the previous version (${previous.validFrom}).`,
|
|
10644
|
+
params: {
|
|
10645
|
+
validFrom: validFrom.toISOString(),
|
|
10646
|
+
previousValidFrom: previous.validFrom
|
|
10647
|
+
}
|
|
9936
10648
|
});
|
|
9937
10649
|
}
|
|
9938
10650
|
if (previous.validUntil) {
|
|
@@ -9941,8 +10653,11 @@ var PlanVersionsService = class _PlanVersionsService {
|
|
|
9941
10653
|
const requiredStart = new Date(prevUntil.getTime() + dayMs);
|
|
9942
10654
|
if (validFrom.getTime() !== requiredStart.getTime()) {
|
|
9943
10655
|
throw new import_common55.UnprocessableEntityException({
|
|
9944
|
-
code:
|
|
9945
|
-
message: `
|
|
10656
|
+
code: import_types25.CATALOG_ERROR_CODES.PLAN_VERSION_VALID_FROM_NOT_GAPLESS,
|
|
10657
|
+
message: `The predecessor has validUntil=${previous.validUntil.slice(0, 10)} \u2014 the successor must start seamlessly on the next day (${requiredStart.toISOString().slice(0, 10)}). Received: ${validFrom.toISOString().slice(0, 10)}.`,
|
|
10658
|
+
params: {
|
|
10659
|
+
received: validFrom.toISOString().slice(0, 10)
|
|
10660
|
+
},
|
|
9946
10661
|
requiredValidFrom: requiredStart.toISOString().slice(0, 10),
|
|
9947
10662
|
previousValidUntil: previous.validUntil
|
|
9948
10663
|
});
|
|
@@ -9953,14 +10668,21 @@ var PlanVersionsService = class _PlanVersionsService {
|
|
|
9953
10668
|
const validUntil = validUntilInput ? new Date(validUntilInput) : null;
|
|
9954
10669
|
if (validUntil && Number.isNaN(validUntil.getTime())) {
|
|
9955
10670
|
throw new import_common55.UnprocessableEntityException({
|
|
9956
|
-
code:
|
|
9957
|
-
message: `validUntil '${validUntilInput}'
|
|
10671
|
+
code: import_types25.CATALOG_ERROR_CODES.PLAN_VERSION_VALID_UNTIL_INVALID,
|
|
10672
|
+
message: `validUntil '${validUntilInput}' is not a valid date`,
|
|
10673
|
+
params: {
|
|
10674
|
+
validUntil: validUntilInput
|
|
10675
|
+
}
|
|
9958
10676
|
});
|
|
9959
10677
|
}
|
|
9960
10678
|
if (validUntil && validUntil <= validFrom) {
|
|
9961
10679
|
throw new import_common55.UnprocessableEntityException({
|
|
9962
|
-
code:
|
|
9963
|
-
message: `validUntil (${validUntil.toISOString()})
|
|
10680
|
+
code: import_types25.CATALOG_ERROR_CODES.PLAN_VERSION_VALID_UNTIL_BEFORE_FROM,
|
|
10681
|
+
message: `validUntil (${validUntil.toISOString()}) must be strictly after validFrom (${validFrom.toISOString()}).`,
|
|
10682
|
+
params: {
|
|
10683
|
+
validUntil: validUntil.toISOString(),
|
|
10684
|
+
validFrom: validFrom.toISOString()
|
|
10685
|
+
}
|
|
9964
10686
|
});
|
|
9965
10687
|
}
|
|
9966
10688
|
const fieldsFor = /* @__PURE__ */ __name((v) => ({
|
|
@@ -9971,14 +10693,14 @@ var PlanVersionsService = class _PlanVersionsService {
|
|
|
9971
10693
|
monthlyNet: v.monthlyNet,
|
|
9972
10694
|
yearlyNet: v.yearlyNet
|
|
9973
10695
|
}), "fieldsFor");
|
|
9974
|
-
const diff = previous ? (0,
|
|
10696
|
+
const diff = previous ? (0, import_types4.classifyPlanDiff)(fieldsFor(previous), fieldsFor(draft)) : {
|
|
9975
10697
|
changes: [],
|
|
9976
10698
|
nonRegressive: true
|
|
9977
10699
|
};
|
|
9978
10700
|
if (!diff.nonRegressive && !publishMeta.forceRegressive) {
|
|
9979
10701
|
throw new import_common55.UnprocessableEntityException({
|
|
9980
|
-
code:
|
|
9981
|
-
message: "
|
|
10702
|
+
code: import_types25.CATALOG_ERROR_CODES.PLAN_VERSION_REGRESSION,
|
|
10703
|
+
message: "This plan version is regressive (feature removed / quota lowered / price raised). Publishing requires an explicit `forceRegressive: true` (UI confirmation dialog with MFA).",
|
|
9982
10704
|
changes: diff.changes
|
|
9983
10705
|
});
|
|
9984
10706
|
}
|
|
@@ -10013,36 +10735,51 @@ var PlanVersionsService = class _PlanVersionsService {
|
|
|
10013
10735
|
async terminatePlanVersion(versionId, endsAt) {
|
|
10014
10736
|
const existing = await this.repo.findVersionById(versionId);
|
|
10015
10737
|
if (!existing) {
|
|
10016
|
-
throw new import_common55.NotFoundException(
|
|
10738
|
+
throw new import_common55.NotFoundException({
|
|
10739
|
+
code: import_types25.CATALOG_ERROR_CODES.PLAN_VERSION_NOT_FOUND,
|
|
10740
|
+
message: `PlanVersion '${versionId}' not found`,
|
|
10741
|
+
params: {
|
|
10742
|
+
versionId
|
|
10743
|
+
}
|
|
10744
|
+
});
|
|
10017
10745
|
}
|
|
10018
10746
|
if (existing.publishedAt === null) {
|
|
10019
10747
|
throw new import_common55.UnprocessableEntityException({
|
|
10020
|
-
code:
|
|
10021
|
-
message: `PlanVersion '${versionId}'
|
|
10748
|
+
code: import_types25.CATALOG_ERROR_CODES.PLAN_VERSION_NOT_LIVE,
|
|
10749
|
+
message: `PlanVersion '${versionId}' is not published and cannot be terminated.`,
|
|
10750
|
+
params: {
|
|
10751
|
+
versionId
|
|
10752
|
+
}
|
|
10022
10753
|
});
|
|
10023
10754
|
}
|
|
10024
10755
|
if (existing.supersededAt !== null) {
|
|
10025
10756
|
throw new import_common55.UnprocessableEntityException({
|
|
10026
|
-
code:
|
|
10027
|
-
message: `PlanVersion '${versionId}'
|
|
10757
|
+
code: import_types25.CATALOG_ERROR_CODES.PLAN_VERSION_NOT_LIVE,
|
|
10758
|
+
message: `PlanVersion '${versionId}' has already been superseded by a newer version (supersededAt) and cannot be terminated.`,
|
|
10759
|
+
params: {
|
|
10760
|
+
versionId
|
|
10761
|
+
}
|
|
10028
10762
|
});
|
|
10029
10763
|
}
|
|
10030
10764
|
if (Number.isNaN(endsAt.getTime())) {
|
|
10031
10765
|
throw new import_common55.UnprocessableEntityException({
|
|
10032
|
-
code:
|
|
10033
|
-
message: "endsAt
|
|
10766
|
+
code: import_types25.CATALOG_ERROR_CODES.PLAN_TERMINATE_INVALID_DATE,
|
|
10767
|
+
message: "endsAt is not a valid date."
|
|
10034
10768
|
});
|
|
10035
10769
|
}
|
|
10036
10770
|
if (endsAt.getTime() <= Date.now()) {
|
|
10037
10771
|
throw new import_common55.UnprocessableEntityException({
|
|
10038
|
-
code:
|
|
10039
|
-
message: `endsAt (${endsAt.toISOString()})
|
|
10772
|
+
code: import_types25.CATALOG_ERROR_CODES.PLAN_TERMINATE_DATE_NOT_FUTURE,
|
|
10773
|
+
message: `endsAt (${endsAt.toISOString()}) must lie strictly in the future.`,
|
|
10774
|
+
params: {
|
|
10775
|
+
endsAt: endsAt.toISOString()
|
|
10776
|
+
}
|
|
10040
10777
|
});
|
|
10041
10778
|
}
|
|
10042
10779
|
if (typeof this.repo.terminate !== "function") {
|
|
10043
10780
|
throw new import_common55.UnprocessableEntityException({
|
|
10044
|
-
code:
|
|
10045
|
-
message: "Terminate
|
|
10781
|
+
code: import_types25.CATALOG_ERROR_CODES.PLAN_TERMINATE_NOT_IMPLEMENTED,
|
|
10782
|
+
message: "Terminate is not implemented in the current repository. Implement PlanRepository.terminate."
|
|
10046
10783
|
});
|
|
10047
10784
|
}
|
|
10048
10785
|
return this.repo.terminate(versionId, endsAt);
|
|
@@ -10060,8 +10797,8 @@ var PlanVersionsService = class _PlanVersionsService {
|
|
|
10060
10797
|
if (blockingStrictModeWarnings(warnings).length === 0) return;
|
|
10061
10798
|
if (this.mode === "blocking") {
|
|
10062
10799
|
throw new import_common55.UnprocessableEntityException({
|
|
10063
|
-
code:
|
|
10064
|
-
message: "
|
|
10800
|
+
code: import_types25.CATALOG_ERROR_CODES.STRICT_MODE_VIOLATIONS,
|
|
10801
|
+
message: "The strict-mode check found drift against the discovery snapshot.",
|
|
10065
10802
|
warnings
|
|
10066
10803
|
});
|
|
10067
10804
|
}
|
|
@@ -10128,7 +10865,7 @@ var CreateBundleDto = class {
|
|
|
10128
10865
|
_ts_decorate60([
|
|
10129
10866
|
(0, import_class_validator9.IsString)(),
|
|
10130
10867
|
(0, import_class_validator9.Matches)(PROJECT_KEY_PATTERN4, {
|
|
10131
|
-
message: 'projectKey
|
|
10868
|
+
message: 'projectKey must be kebab-case (e.g. "my-app")'
|
|
10132
10869
|
}),
|
|
10133
10870
|
(0, import_class_validator9.MaxLength)(64),
|
|
10134
10871
|
_ts_metadata47("design:type", String)
|
|
@@ -10136,7 +10873,7 @@ _ts_decorate60([
|
|
|
10136
10873
|
_ts_decorate60([
|
|
10137
10874
|
(0, import_class_validator9.IsString)(),
|
|
10138
10875
|
(0, import_class_validator9.Matches)(KEY_PATTERN, {
|
|
10139
|
-
message: 'bundleKey
|
|
10876
|
+
message: 'bundleKey must be SCREAMING_SNAKE_CASE (e.g. "BANKING")'
|
|
10140
10877
|
}),
|
|
10141
10878
|
(0, import_class_validator9.MaxLength)(64),
|
|
10142
10879
|
_ts_metadata47("design:type", String)
|
|
@@ -10236,7 +10973,7 @@ _ts_decorate60([
|
|
|
10236
10973
|
}),
|
|
10237
10974
|
(0, import_class_validator9.Matches)(FEATURE_KEY_PATTERN, {
|
|
10238
10975
|
each: true,
|
|
10239
|
-
message: "features
|
|
10976
|
+
message: "features entries must be SCREAMING_SNAKE_CASE"
|
|
10240
10977
|
}),
|
|
10241
10978
|
_ts_metadata47("design:type", Array)
|
|
10242
10979
|
], CreateBundleVersionDraftDto.prototype, "features", void 0);
|
|
@@ -10262,7 +10999,7 @@ _ts_decorate60([
|
|
|
10262
10999
|
no_symbols: false
|
|
10263
11000
|
}),
|
|
10264
11001
|
(0, import_class_validator9.Matches)(DECIMAL_PATTERN, {
|
|
10265
|
-
message: 'monthlyNet
|
|
11002
|
+
message: 'monthlyNet must be a decimal with at most 2 fraction digits (e.g. "9.90")'
|
|
10266
11003
|
}),
|
|
10267
11004
|
_ts_metadata47("design:type", Object)
|
|
10268
11005
|
], CreateBundleVersionDraftDto.prototype, "monthlyNet", void 0);
|
|
@@ -10273,7 +11010,7 @@ _ts_decorate60([
|
|
|
10273
11010
|
no_symbols: false
|
|
10274
11011
|
}),
|
|
10275
11012
|
(0, import_class_validator9.Matches)(DECIMAL_PATTERN, {
|
|
10276
|
-
message: "yearlyNet
|
|
11013
|
+
message: "yearlyNet must be a decimal with at most 2 fraction digits"
|
|
10277
11014
|
}),
|
|
10278
11015
|
_ts_metadata47("design:type", Object)
|
|
10279
11016
|
], CreateBundleVersionDraftDto.prototype, "yearlyNet", void 0);
|
|
@@ -10297,7 +11034,7 @@ _ts_decorate60([
|
|
|
10297
11034
|
(0, import_class_validator9.IsOptional)(),
|
|
10298
11035
|
(0, import_class_validator9.ValidateIf)((_o, value) => value !== null),
|
|
10299
11036
|
(0, import_class_validator9.Matches)(ISO_DATE_PATTERN3, {
|
|
10300
|
-
message: "validFrom
|
|
11037
|
+
message: "validFrom must be an ISO date (YYYY-MM-DD)"
|
|
10301
11038
|
}),
|
|
10302
11039
|
_ts_metadata47("design:type", Object)
|
|
10303
11040
|
], CreateBundleVersionDraftDto.prototype, "validFrom", void 0);
|
|
@@ -10305,7 +11042,7 @@ _ts_decorate60([
|
|
|
10305
11042
|
(0, import_class_validator9.IsOptional)(),
|
|
10306
11043
|
(0, import_class_validator9.ValidateIf)((_o, value) => value !== null),
|
|
10307
11044
|
(0, import_class_validator9.Matches)(ISO_DATE_PATTERN3, {
|
|
10308
|
-
message: "validUntil
|
|
11045
|
+
message: "validUntil must be an ISO date (YYYY-MM-DD)"
|
|
10309
11046
|
}),
|
|
10310
11047
|
_ts_metadata47("design:type", Object)
|
|
10311
11048
|
], CreateBundleVersionDraftDto.prototype, "validUntil", void 0);
|
|
@@ -10377,7 +11114,7 @@ _ts_decorate60([
|
|
|
10377
11114
|
(0, import_class_validator9.IsOptional)(),
|
|
10378
11115
|
(0, import_class_validator9.ValidateIf)((_o, value) => value !== null),
|
|
10379
11116
|
(0, import_class_validator9.Matches)(ISO_DATE_PATTERN3, {
|
|
10380
|
-
message: "validFrom
|
|
11117
|
+
message: "validFrom must be an ISO date (YYYY-MM-DD)"
|
|
10381
11118
|
}),
|
|
10382
11119
|
_ts_metadata47("design:type", Object)
|
|
10383
11120
|
], UpdateBundleVersionDraftDto.prototype, "validFrom", void 0);
|
|
@@ -10385,7 +11122,7 @@ _ts_decorate60([
|
|
|
10385
11122
|
(0, import_class_validator9.IsOptional)(),
|
|
10386
11123
|
(0, import_class_validator9.ValidateIf)((_o, value) => value !== null),
|
|
10387
11124
|
(0, import_class_validator9.Matches)(ISO_DATE_PATTERN3, {
|
|
10388
|
-
message: "validUntil
|
|
11125
|
+
message: "validUntil must be an ISO date (YYYY-MM-DD)"
|
|
10389
11126
|
}),
|
|
10390
11127
|
_ts_metadata47("design:type", Object)
|
|
10391
11128
|
], UpdateBundleVersionDraftDto.prototype, "validUntil", void 0);
|
|
@@ -10422,7 +11159,7 @@ _ts_decorate60([
|
|
|
10422
11159
|
(0, import_class_validator9.IsOptional)(),
|
|
10423
11160
|
(0, import_class_validator9.ValidateIf)((_o, value) => value !== null),
|
|
10424
11161
|
(0, import_class_validator9.Matches)(ISO_DATE_PATTERN3, {
|
|
10425
|
-
message: "validFrom
|
|
11162
|
+
message: "validFrom must be an ISO date (YYYY-MM-DD)"
|
|
10426
11163
|
}),
|
|
10427
11164
|
_ts_metadata47("design:type", Object)
|
|
10428
11165
|
], PublishBundleVersionDto.prototype, "validFrom", void 0);
|
|
@@ -10430,7 +11167,7 @@ _ts_decorate60([
|
|
|
10430
11167
|
(0, import_class_validator9.IsOptional)(),
|
|
10431
11168
|
(0, import_class_validator9.ValidateIf)((_o, value) => value !== null),
|
|
10432
11169
|
(0, import_class_validator9.Matches)(ISO_DATE_PATTERN3, {
|
|
10433
|
-
message: "validUntil
|
|
11170
|
+
message: "validUntil must be an ISO date (YYYY-MM-DD)"
|
|
10434
11171
|
}),
|
|
10435
11172
|
_ts_metadata47("design:type", Object)
|
|
10436
11173
|
], PublishBundleVersionDto.prototype, "validUntil", void 0);
|
|
@@ -10734,7 +11471,7 @@ var CreateMarketingProjectionDto = class {
|
|
|
10734
11471
|
_ts_decorate62([
|
|
10735
11472
|
(0, import_class_validator10.IsString)(),
|
|
10736
11473
|
(0, import_class_validator10.Matches)(PROJECT_KEY_PATTERN5, {
|
|
10737
|
-
message: "projectKey
|
|
11474
|
+
message: "projectKey must be kebab-case"
|
|
10738
11475
|
}),
|
|
10739
11476
|
(0, import_class_validator10.MaxLength)(64),
|
|
10740
11477
|
_ts_metadata49("design:type", String)
|
|
@@ -10752,7 +11489,7 @@ _ts_decorate62([
|
|
|
10752
11489
|
(0, import_class_validator10.IsOptional)(),
|
|
10753
11490
|
(0, import_class_validator10.IsString)(),
|
|
10754
11491
|
(0, import_class_validator10.Matches)(LOCALE_PATTERN2, {
|
|
10755
|
-
message: 'locale
|
|
11492
|
+
message: 'locale must be ISO-639-1 (e.g. "de", "en", "de-AT")'
|
|
10756
11493
|
}),
|
|
10757
11494
|
_ts_metadata49("design:type", String)
|
|
10758
11495
|
], CreateMarketingProjectionDto.prototype, "locale", void 0);
|
|
@@ -11083,7 +11820,7 @@ var CreatePlanDto = class {
|
|
|
11083
11820
|
_ts_decorate64([
|
|
11084
11821
|
(0, import_class_validator11.IsString)(),
|
|
11085
11822
|
(0, import_class_validator11.Matches)(PROJECT_KEY_PATTERN6, {
|
|
11086
|
-
message: 'projectKey
|
|
11823
|
+
message: 'projectKey must be kebab-case (e.g. "my-app")'
|
|
11087
11824
|
}),
|
|
11088
11825
|
(0, import_class_validator11.MaxLength)(64),
|
|
11089
11826
|
_ts_metadata51("design:type", String)
|
|
@@ -11091,7 +11828,7 @@ _ts_decorate64([
|
|
|
11091
11828
|
_ts_decorate64([
|
|
11092
11829
|
(0, import_class_validator11.IsString)(),
|
|
11093
11830
|
(0, import_class_validator11.Matches)(KEY_PATTERN2, {
|
|
11094
|
-
message: 'planKey
|
|
11831
|
+
message: 'planKey must be SCREAMING_SNAKE_CASE (e.g. "STARTER")'
|
|
11095
11832
|
}),
|
|
11096
11833
|
(0, import_class_validator11.MaxLength)(64),
|
|
11097
11834
|
_ts_metadata51("design:type", String)
|
|
@@ -11200,7 +11937,7 @@ _ts_decorate65([
|
|
|
11200
11937
|
}),
|
|
11201
11938
|
(0, import_class_validator12.Matches)(FEATURE_KEY_PATTERN2, {
|
|
11202
11939
|
each: true,
|
|
11203
|
-
message: "features
|
|
11940
|
+
message: "features entries must be SCREAMING_SNAKE_CASE"
|
|
11204
11941
|
}),
|
|
11205
11942
|
_ts_metadata52("design:type", Array)
|
|
11206
11943
|
], CreatePlanVersionDraftDto.prototype, "features", void 0);
|
|
@@ -11212,7 +11949,7 @@ _ts_decorate65([
|
|
|
11212
11949
|
}),
|
|
11213
11950
|
(0, import_class_validator12.Matches)(FEATURE_KEY_PATTERN2, {
|
|
11214
11951
|
each: true,
|
|
11215
|
-
message: "bundles
|
|
11952
|
+
message: "bundles entries must be SCREAMING_SNAKE_CASE"
|
|
11216
11953
|
}),
|
|
11217
11954
|
_ts_metadata52("design:type", Array)
|
|
11218
11955
|
], CreatePlanVersionDraftDto.prototype, "bundles", void 0);
|
|
@@ -11222,13 +11959,13 @@ _ts_decorate65([
|
|
|
11222
11959
|
], CreatePlanVersionDraftDto.prototype, "quotas", void 0);
|
|
11223
11960
|
_ts_decorate65([
|
|
11224
11961
|
(0, import_class_validator12.Matches)(DECIMAL_PATTERN2, {
|
|
11225
|
-
message: 'monthlyNet
|
|
11962
|
+
message: 'monthlyNet must be a decimal with at most 2 fraction digits (e.g. "9.90")'
|
|
11226
11963
|
}),
|
|
11227
11964
|
_ts_metadata52("design:type", String)
|
|
11228
11965
|
], CreatePlanVersionDraftDto.prototype, "monthlyNet", void 0);
|
|
11229
11966
|
_ts_decorate65([
|
|
11230
11967
|
(0, import_class_validator12.Matches)(DECIMAL_PATTERN2, {
|
|
11231
|
-
message: "yearlyNet
|
|
11968
|
+
message: "yearlyNet must be a decimal with at most 2 fraction digits"
|
|
11232
11969
|
}),
|
|
11233
11970
|
_ts_metadata52("design:type", String)
|
|
11234
11971
|
], CreatePlanVersionDraftDto.prototype, "yearlyNet", void 0);
|
|
@@ -11472,7 +12209,7 @@ function buildPlansController(guards) {
|
|
|
11472
12209
|
}
|
|
11473
12210
|
assertVersionsService() {
|
|
11474
12211
|
if (!this.versionsService) {
|
|
11475
|
-
throw new Error("PlanVersionsService
|
|
12212
|
+
throw new Error("PlanVersionsService is not registered \u2014 the app has not enabled the plan lifecycle editor.");
|
|
11476
12213
|
}
|
|
11477
12214
|
}
|
|
11478
12215
|
};
|
|
@@ -11837,9 +12574,9 @@ function runPreflight(input) {
|
|
|
11837
12574
|
__name(runPreflight, "runPreflight");
|
|
11838
12575
|
function formatPreflightReport(report) {
|
|
11839
12576
|
const lines = [];
|
|
11840
|
-
lines.push(`Preflight (Status: ${report.overall.toUpperCase()})`, ` Plans: ${report.counts.planFindings} \xB7 Bundles: ${report.counts.bundleFindings} \xB7
|
|
12577
|
+
lines.push(`Preflight (Status: ${report.overall.toUpperCase()})`, ` Plans: ${report.counts.planFindings} \xB7 Bundles: ${report.counts.bundleFindings} \xB7 Total: ${report.counts.total}`, "");
|
|
11841
12578
|
if (report.findings.length === 0) {
|
|
11842
|
-
lines.push(" \u2713
|
|
12579
|
+
lines.push(" \u2713 No strict-mode violations found.");
|
|
11843
12580
|
return lines.join("\n");
|
|
11844
12581
|
}
|
|
11845
12582
|
let lastEntity = "";
|
|
@@ -11909,9 +12646,9 @@ function validateSeedAgainstSnapshot(input) {
|
|
|
11909
12646
|
__name(validateSeedAgainstSnapshot, "validateSeedAgainstSnapshot");
|
|
11910
12647
|
function formatSeedGateReport(report) {
|
|
11911
12648
|
const lines = [];
|
|
11912
|
-
lines.push(`Seed-Gate (Status: ${report.overall.toUpperCase()})`, ` Plans: ${report.counts.planFindings} \xB7 Bundles: ${report.counts.bundleFindings} \xB7
|
|
12649
|
+
lines.push(`Seed-Gate (Status: ${report.overall.toUpperCase()})`, ` Plans: ${report.counts.planFindings} \xB7 Bundles: ${report.counts.bundleFindings} \xB7 Total: ${report.counts.total}`, "");
|
|
11913
12650
|
if (report.findings.length === 0) {
|
|
11914
|
-
lines.push(" \u2713
|
|
12651
|
+
lines.push(" \u2713 Every seeded plan/bundle feature is discovered (and approved where checked).");
|
|
11915
12652
|
return lines.join("\n");
|
|
11916
12653
|
}
|
|
11917
12654
|
let lastEntity = "";
|
|
@@ -11941,7 +12678,7 @@ var DiscoverySnapshotNotFoundError = class extends Error {
|
|
|
11941
12678
|
}
|
|
11942
12679
|
path;
|
|
11943
12680
|
constructor(path) {
|
|
11944
|
-
super(`Discovery
|
|
12681
|
+
super(`Discovery snapshot file not found: ${path}. Boot the app first with DISCOVERY_SNAPSHOT_PATH or DiscoveryModule.forRoot({ snapshotPath: '\u2026' }) so that the scanner writes the file on the OnApplicationBootstrap hook.`), this.path = path;
|
|
11945
12682
|
this.name = "DiscoverySnapshotNotFoundError";
|
|
11946
12683
|
}
|
|
11947
12684
|
};
|
|
@@ -11968,10 +12705,10 @@ function runSeedGateFromFile(options) {
|
|
|
11968
12705
|
} catch (err) {
|
|
11969
12706
|
if (err instanceof DiscoverySnapshotNotFoundError) {
|
|
11970
12707
|
if (mode === "blocking") {
|
|
11971
|
-
error(`[seed-gate]
|
|
12708
|
+
error(`[seed-gate] ERROR: no discovery snapshot at ${options.snapshotPath} \u2014 required in blocking mode (run the headless scan before seeding, #23).`);
|
|
11972
12709
|
return exit(4);
|
|
11973
12710
|
}
|
|
11974
|
-
warn(`[seed-gate]
|
|
12711
|
+
warn(`[seed-gate] No discovery snapshot at ${options.snapshotPath} \u2014 skipped (report-only).`);
|
|
11975
12712
|
return null;
|
|
11976
12713
|
}
|
|
11977
12714
|
throw err;
|
|
@@ -11985,10 +12722,10 @@ function runSeedGateFromFile(options) {
|
|
|
11985
12722
|
log(formatSeedGateReport(report));
|
|
11986
12723
|
if (report.overall === "error") {
|
|
11987
12724
|
if (mode === "blocking") {
|
|
11988
|
-
error("[seed-gate] blocking:
|
|
12725
|
+
error("[seed-gate] blocking: violations found \u2014 aborting the seed (#23).");
|
|
11989
12726
|
return exit(seedGateExitCode(report));
|
|
11990
12727
|
}
|
|
11991
|
-
warn("[seed-gate] report-only:
|
|
12728
|
+
warn("[seed-gate] report-only: violations found, seeding continues anyway.");
|
|
11992
12729
|
}
|
|
11993
12730
|
return report;
|
|
11994
12731
|
}
|
|
@@ -12007,7 +12744,7 @@ __export(checkout_offer_exports, {
|
|
|
12007
12744
|
|
|
12008
12745
|
// src/checkout-offer/checkout-offer.service.ts
|
|
12009
12746
|
var import_common60 = require("@nestjs/common");
|
|
12010
|
-
var
|
|
12747
|
+
var import_types26 = require("@saasicat/types");
|
|
12011
12748
|
|
|
12012
12749
|
// src/checkout-offer/tokens.ts
|
|
12013
12750
|
var CHECKOUT_OFFER_REPOSITORY_TOKEN = /* @__PURE__ */ Symbol.for("saas-platform/CheckoutOfferRepository");
|
|
@@ -12050,7 +12787,13 @@ var CheckoutOfferService = class {
|
|
|
12050
12787
|
async getById(id) {
|
|
12051
12788
|
const row = await this.repo.findById(id);
|
|
12052
12789
|
if (!row) {
|
|
12053
|
-
throw new import_common60.NotFoundException(
|
|
12790
|
+
throw new import_common60.NotFoundException({
|
|
12791
|
+
code: import_types26.CONTRACT_ERROR_CODES.CHECKOUT_OFFER_NOT_FOUND,
|
|
12792
|
+
message: `CheckoutOffer '${id}' not found`,
|
|
12793
|
+
params: {
|
|
12794
|
+
offerId: id
|
|
12795
|
+
}
|
|
12796
|
+
});
|
|
12054
12797
|
}
|
|
12055
12798
|
return row;
|
|
12056
12799
|
}
|
|
@@ -12068,7 +12811,7 @@ var CheckoutOfferService = class {
|
|
|
12068
12811
|
/** Customization in onboarding — only while the offer is `open`. */
|
|
12069
12812
|
async update(id, data) {
|
|
12070
12813
|
const existing = await this.getById(id);
|
|
12071
|
-
this.assertOpen(existing, "
|
|
12814
|
+
this.assertOpen(existing, "changed");
|
|
12072
12815
|
const next = this.normalizeUpdateData(existing, data);
|
|
12073
12816
|
await this.assertFeatureRequiresSatisfied({
|
|
12074
12817
|
projectKey: existing.projectKey,
|
|
@@ -12086,7 +12829,7 @@ var CheckoutOfferService = class {
|
|
|
12086
12829
|
*/
|
|
12087
12830
|
async consume(id) {
|
|
12088
12831
|
const existing = await this.getById(id);
|
|
12089
|
-
this.assertOpen(existing, "
|
|
12832
|
+
this.assertOpen(existing, "consumed");
|
|
12090
12833
|
await this.assertBundleVersionsStillBookable(existing);
|
|
12091
12834
|
return this.repo.consume(id);
|
|
12092
12835
|
}
|
|
@@ -12104,19 +12847,19 @@ var CheckoutOfferService = class {
|
|
|
12104
12847
|
const entries = await this.catalogEntries.listFeatures({
|
|
12105
12848
|
projectKey: input.projectKey
|
|
12106
12849
|
});
|
|
12107
|
-
const requiresIndex = (0,
|
|
12850
|
+
const requiresIndex = (0, import_types26.buildFeatureRequiresIndex)(entries);
|
|
12108
12851
|
if (requiresIndex.size === 0) return;
|
|
12109
12852
|
const selected = /* @__PURE__ */ new Set([
|
|
12110
12853
|
...await this.resolvePlanFeatures(input),
|
|
12111
12854
|
...await this.resolveBundleFeatures(input)
|
|
12112
12855
|
]);
|
|
12113
|
-
const missingRequires = (0,
|
|
12856
|
+
const missingRequires = (0, import_types26.collectUnsatisfiedRequires)([
|
|
12114
12857
|
...selected
|
|
12115
12858
|
], requiresIndex);
|
|
12116
12859
|
if (missingRequires.length > 0) {
|
|
12117
12860
|
throw new import_common60.UnprocessableEntityException({
|
|
12118
|
-
code:
|
|
12119
|
-
message: `
|
|
12861
|
+
code: import_types26.CONTRACT_ERROR_CODES.CHECKOUT_OFFER_FEATURE_DEPENDENCY_UNSATISFIED,
|
|
12862
|
+
message: `The selected plan does not cover all feature dependencies: [${missingRequires.join(", ")}] are missing from the plan + selected bundles.`,
|
|
12120
12863
|
missingRequires
|
|
12121
12864
|
});
|
|
12122
12865
|
}
|
|
@@ -12196,8 +12939,8 @@ var CheckoutOfferService = class {
|
|
|
12196
12939
|
}
|
|
12197
12940
|
if (violations.length > 0) {
|
|
12198
12941
|
throw new import_common60.UnprocessableEntityException({
|
|
12199
|
-
code:
|
|
12200
|
-
message: "
|
|
12942
|
+
code: import_types26.CONTRACT_ERROR_CODES.CHECKOUT_OFFER_BUNDLE_VERSION_NOT_BOOKABLE,
|
|
12943
|
+
message: "At least one bundle version from the checkout offer is no longer bookable.",
|
|
12201
12944
|
violations
|
|
12202
12945
|
});
|
|
12203
12946
|
}
|
|
@@ -12251,15 +12994,15 @@ var CheckoutOfferService = class {
|
|
|
12251
12994
|
const hasPlan = lineItems.some((item) => item.kind === "plan");
|
|
12252
12995
|
if (!hasPlan) {
|
|
12253
12996
|
throw new import_common60.UnprocessableEntityException({
|
|
12254
|
-
code:
|
|
12255
|
-
message: "
|
|
12997
|
+
code: import_types26.CONTRACT_ERROR_CODES.CHECKOUT_OFFER_PLAN_LINE_ITEM_REQUIRED,
|
|
12998
|
+
message: "A checkout offer requires a frozen plan line item."
|
|
12256
12999
|
});
|
|
12257
13000
|
}
|
|
12258
13001
|
const missingBundleVersionIds = input.bundleVersionIds.filter((versionId) => !lineItems.some((item) => item.kind === "bundle" && item.sourceVersionId === versionId));
|
|
12259
13002
|
if (missingBundleVersionIds.length > 0) {
|
|
12260
13003
|
throw new import_common60.UnprocessableEntityException({
|
|
12261
|
-
code:
|
|
12262
|
-
message: "
|
|
13004
|
+
code: import_types26.CONTRACT_ERROR_CODES.CHECKOUT_OFFER_BUNDLE_LINE_ITEMS_REQUIRED,
|
|
13005
|
+
message: "Every selected bundle version requires a frozen bundle line item.",
|
|
12263
13006
|
bundleVersionIds: missingBundleVersionIds
|
|
12264
13007
|
});
|
|
12265
13008
|
}
|
|
@@ -12291,13 +13034,35 @@ var CheckoutOfferService = class {
|
|
|
12291
13034
|
}
|
|
12292
13035
|
assertOpen(existing, action) {
|
|
12293
13036
|
if (existing.status === "consumed") {
|
|
12294
|
-
throw new import_common60.ConflictException(
|
|
13037
|
+
throw new import_common60.ConflictException({
|
|
13038
|
+
code: import_types26.CONTRACT_ERROR_CODES.CHECKOUT_OFFER_ALREADY_CONSUMED,
|
|
13039
|
+
message: `Checkout offer '${existing.id}' has already been consumed and cannot be ${action}`,
|
|
13040
|
+
params: {
|
|
13041
|
+
offerId: existing.id,
|
|
13042
|
+
action
|
|
13043
|
+
}
|
|
13044
|
+
});
|
|
12295
13045
|
}
|
|
12296
13046
|
if (existing.status === "expired") {
|
|
12297
|
-
throw new import_common60.ConflictException(
|
|
13047
|
+
throw new import_common60.ConflictException({
|
|
13048
|
+
code: import_types26.CONTRACT_ERROR_CODES.CHECKOUT_OFFER_EXPIRED,
|
|
13049
|
+
message: `Checkout offer '${existing.id}' has expired and cannot be ${action}`,
|
|
13050
|
+
params: {
|
|
13051
|
+
offerId: existing.id,
|
|
13052
|
+
action
|
|
13053
|
+
}
|
|
13054
|
+
});
|
|
12298
13055
|
}
|
|
12299
13056
|
if (this.isExpired(existing)) {
|
|
12300
|
-
throw new import_common60.ConflictException(
|
|
13057
|
+
throw new import_common60.ConflictException({
|
|
13058
|
+
code: import_types26.CONTRACT_ERROR_CODES.CHECKOUT_OFFER_EXPIRED,
|
|
13059
|
+
message: `CheckoutOffer '${existing.id}' has expired and cannot be ${action}`,
|
|
13060
|
+
params: {
|
|
13061
|
+
offerId: existing.id,
|
|
13062
|
+
action,
|
|
13063
|
+
validUntil: existing.validUntil ?? null
|
|
13064
|
+
}
|
|
13065
|
+
});
|
|
12301
13066
|
}
|
|
12302
13067
|
}
|
|
12303
13068
|
isExpired(row) {
|
|
@@ -12315,7 +13080,7 @@ var CheckoutOfferService = class {
|
|
|
12315
13080
|
if (!value) return false;
|
|
12316
13081
|
const validUntil = new Date(value).getTime();
|
|
12317
13082
|
if (Number.isNaN(validUntil)) return true;
|
|
12318
|
-
return validUntil < (0,
|
|
13083
|
+
return validUntil < (0, import_types26.startOfUtcDay)(new Date(nowMs)).getTime();
|
|
12319
13084
|
}
|
|
12320
13085
|
};
|
|
12321
13086
|
CheckoutOfferService = _ts_decorate68([
|
|
@@ -12379,7 +13144,7 @@ var CreateCheckoutOfferDto = class {
|
|
|
12379
13144
|
_ts_decorate69([
|
|
12380
13145
|
(0, import_class_validator13.IsString)(),
|
|
12381
13146
|
(0, import_class_validator13.Matches)(PROJECT_KEY_PATTERN7, {
|
|
12382
|
-
message: "projectKey
|
|
13147
|
+
message: "projectKey must be kebab-case"
|
|
12383
13148
|
}),
|
|
12384
13149
|
(0, import_class_validator13.MaxLength)(64),
|
|
12385
13150
|
_ts_metadata55("design:type", String)
|
|
@@ -12867,7 +13632,7 @@ async function runHeadlessDiscoveryScan(appModule, options = {}) {
|
|
|
12867
13632
|
await app.close();
|
|
12868
13633
|
}
|
|
12869
13634
|
} catch (err) {
|
|
12870
|
-
console.error("[discovery-scan]
|
|
13635
|
+
console.error("[discovery-scan] ERROR:", err instanceof Error ? err.message : err);
|
|
12871
13636
|
return exit(4);
|
|
12872
13637
|
}
|
|
12873
13638
|
}
|
|
@@ -12946,7 +13711,7 @@ __export(platform_exports, {
|
|
|
12946
13711
|
// src/platform/saas-platform.module.ts
|
|
12947
13712
|
var import_common79 = require("@nestjs/common");
|
|
12948
13713
|
var import_core9 = require("@nestjs/core");
|
|
12949
|
-
var
|
|
13714
|
+
var import_types30 = require("@saasicat/types");
|
|
12950
13715
|
|
|
12951
13716
|
// src/promo/module.ts
|
|
12952
13717
|
var import_common70 = require("@nestjs/common");
|
|
@@ -12987,7 +13752,7 @@ var PromoCodeExpirer = class _PromoCodeExpirer {
|
|
|
12987
13752
|
const codes = await this.promoRepo.expireDueCodes(now);
|
|
12988
13753
|
const redemptions = await this.redemptionRepo.expireDueRedemptions(now);
|
|
12989
13754
|
if (codes > 0 || redemptions > 0) {
|
|
12990
|
-
this.logger.log(`PromoCodeExpirer: ${codes} Codes, ${redemptions}
|
|
13755
|
+
this.logger.log(`PromoCodeExpirer: ${codes} Codes, ${redemptions} redemptions expired.`);
|
|
12991
13756
|
}
|
|
12992
13757
|
}
|
|
12993
13758
|
};
|
|
@@ -13016,6 +13781,7 @@ var import_common68 = require("@nestjs/common");
|
|
|
13016
13781
|
|
|
13017
13782
|
// src/promo/rate-limit.guard.ts
|
|
13018
13783
|
var import_common67 = require("@nestjs/common");
|
|
13784
|
+
var import_types27 = require("@saasicat/types");
|
|
13019
13785
|
function _ts_decorate75(decorators, target, key, desc) {
|
|
13020
13786
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
13021
13787
|
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
@@ -13027,6 +13793,15 @@ var IP_WINDOW_MS = 6e4;
|
|
|
13027
13793
|
var IP_LIMIT = 20;
|
|
13028
13794
|
var SESSION_WINDOW_MS = 60 * 6e4;
|
|
13029
13795
|
var SESSION_LIMIT = 50;
|
|
13796
|
+
function rateLimited() {
|
|
13797
|
+
return new import_common67.HttpException({
|
|
13798
|
+
valid: false,
|
|
13799
|
+
// Superseded by `code` — kept until all consumers read `code`.
|
|
13800
|
+
reason: "RATE_LIMITED",
|
|
13801
|
+
code: import_types27.REGISTRATION_ERROR_CODES.RATE_LIMITED
|
|
13802
|
+
}, import_common67.HttpStatus.TOO_MANY_REQUESTS);
|
|
13803
|
+
}
|
|
13804
|
+
__name(rateLimited, "rateLimited");
|
|
13030
13805
|
var PromoCodeRateLimitGuard = class {
|
|
13031
13806
|
static {
|
|
13032
13807
|
__name(this, "PromoCodeRateLimitGuard");
|
|
@@ -13038,16 +13813,10 @@ var PromoCodeRateLimitGuard = class {
|
|
|
13038
13813
|
const ipKey = ipFingerprint(req);
|
|
13039
13814
|
const sessionKey = req.user?.id ?? null;
|
|
13040
13815
|
if (this.exceeded(this.ipBuckets, ipKey, IP_WINDOW_MS, IP_LIMIT)) {
|
|
13041
|
-
throw
|
|
13042
|
-
valid: false,
|
|
13043
|
-
reason: "RATE_LIMITED"
|
|
13044
|
-
}, import_common67.HttpStatus.TOO_MANY_REQUESTS);
|
|
13816
|
+
throw rateLimited();
|
|
13045
13817
|
}
|
|
13046
13818
|
if (sessionKey && this.exceeded(this.sessionBuckets, sessionKey, SESSION_WINDOW_MS, SESSION_LIMIT)) {
|
|
13047
|
-
throw
|
|
13048
|
-
valid: false,
|
|
13049
|
-
reason: "RATE_LIMITED"
|
|
13050
|
-
}, import_common67.HttpStatus.TOO_MANY_REQUESTS);
|
|
13819
|
+
throw rateLimited();
|
|
13051
13820
|
}
|
|
13052
13821
|
return true;
|
|
13053
13822
|
}
|
|
@@ -13111,21 +13880,21 @@ var PreviewPromoCodeDto = class {
|
|
|
13111
13880
|
_ts_decorate76([
|
|
13112
13881
|
(0, import_class_validator14.IsString)(),
|
|
13113
13882
|
(0, import_class_validator14.Matches)(CODE_PATTERN2, {
|
|
13114
|
-
message: 'code
|
|
13883
|
+
message: 'code must consist of A\u2013Z, 0\u20139, "-" and "_" (4\u201332 characters)'
|
|
13115
13884
|
}),
|
|
13116
13885
|
_ts_metadata59("design:type", String)
|
|
13117
13886
|
], PreviewPromoCodeDto.prototype, "code", void 0);
|
|
13118
13887
|
_ts_decorate76([
|
|
13119
13888
|
(0, import_class_validator14.IsString)(),
|
|
13120
13889
|
(0, import_class_validator14.Matches)(PLAN_OR_CYCLE_PATTERN3, {
|
|
13121
|
-
message: "plan
|
|
13890
|
+
message: "plan must be SCREAMING_SNAKE_CASE"
|
|
13122
13891
|
}),
|
|
13123
13892
|
_ts_metadata59("design:type", String)
|
|
13124
13893
|
], PreviewPromoCodeDto.prototype, "plan", void 0);
|
|
13125
13894
|
_ts_decorate76([
|
|
13126
13895
|
(0, import_class_validator14.IsString)(),
|
|
13127
13896
|
(0, import_class_validator14.Matches)(PLAN_OR_CYCLE_PATTERN3, {
|
|
13128
|
-
message: "billingCycle
|
|
13897
|
+
message: "billingCycle must be SCREAMING_SNAKE_CASE (e.g. MONTHLY, YEARLY)"
|
|
13129
13898
|
}),
|
|
13130
13899
|
_ts_metadata59("design:type", String)
|
|
13131
13900
|
], PreviewPromoCodeDto.prototype, "billingCycle", void 0);
|
|
@@ -13686,7 +14455,7 @@ var import_class_validator16 = require("class-validator");
|
|
|
13686
14455
|
|
|
13687
14456
|
// src/setup/setup.service.ts
|
|
13688
14457
|
var import_common71 = require("@nestjs/common");
|
|
13689
|
-
var
|
|
14458
|
+
var import_types28 = require("@saasicat/types");
|
|
13690
14459
|
var import_node_crypto3 = require("crypto");
|
|
13691
14460
|
var import_qrcode = __toESM(require("qrcode"), 1);
|
|
13692
14461
|
|
|
@@ -13743,7 +14512,7 @@ var SetupService = class {
|
|
|
13743
14512
|
const email = req.email.trim().toLowerCase();
|
|
13744
14513
|
if (!email.includes("@")) {
|
|
13745
14514
|
throw new import_common71.BadRequestException({
|
|
13746
|
-
code:
|
|
14515
|
+
code: import_types28.SETUP_ERROR_CODES.INVALID_EMAIL
|
|
13747
14516
|
});
|
|
13748
14517
|
}
|
|
13749
14518
|
const generated = !req.password;
|
|
@@ -13755,9 +14524,9 @@ var SetupService = class {
|
|
|
13755
14524
|
password
|
|
13756
14525
|
});
|
|
13757
14526
|
} catch (err) {
|
|
13758
|
-
if ((0,
|
|
14527
|
+
if ((0, import_types28.isPlatformUserExistsError)(err)) {
|
|
13759
14528
|
throw new import_common71.ConflictException({
|
|
13760
|
-
code:
|
|
14529
|
+
code: import_types28.SETUP_ERROR_CODES.EMAIL_EXISTS,
|
|
13761
14530
|
message: err.message
|
|
13762
14531
|
});
|
|
13763
14532
|
}
|
|
@@ -13793,20 +14562,23 @@ var SetupService = class {
|
|
|
13793
14562
|
const expected = process.env[this.config.setupTokenEnvVar]?.trim();
|
|
13794
14563
|
if (!expected) {
|
|
13795
14564
|
throw new import_common71.ForbiddenException({
|
|
13796
|
-
code:
|
|
13797
|
-
message: `${this.config.setupTokenEnvVar}
|
|
14565
|
+
code: import_types28.SETUP_ERROR_CODES.SETUP_DISABLED,
|
|
14566
|
+
message: `${this.config.setupTokenEnvVar} is not set \u2014 setup is disabled.`,
|
|
14567
|
+
params: {
|
|
14568
|
+
envVar: this.config.setupTokenEnvVar
|
|
14569
|
+
}
|
|
13798
14570
|
});
|
|
13799
14571
|
}
|
|
13800
14572
|
if (!token || !timingSafeEqualStr(token, expected)) {
|
|
13801
14573
|
throw new import_common71.UnauthorizedException({
|
|
13802
|
-
code:
|
|
14574
|
+
code: import_types28.SETUP_ERROR_CODES.INVALID_SETUP_TOKEN
|
|
13803
14575
|
});
|
|
13804
14576
|
}
|
|
13805
14577
|
}
|
|
13806
14578
|
async assertNoSuperAdmin() {
|
|
13807
14579
|
if (await this.users.countSuperAdmins() > 0) {
|
|
13808
14580
|
throw new import_common71.ConflictException({
|
|
13809
|
-
code:
|
|
14581
|
+
code: import_types28.SETUP_ERROR_CODES.SETUP_ALREADY_DONE
|
|
13810
14582
|
});
|
|
13811
14583
|
}
|
|
13812
14584
|
}
|
|
@@ -14082,6 +14854,7 @@ StaticEntitlementService = _ts_decorate84([
|
|
|
14082
14854
|
// src/platform/static-feature.guard.ts
|
|
14083
14855
|
var import_common75 = require("@nestjs/common");
|
|
14084
14856
|
var import_core7 = require("@nestjs/core");
|
|
14857
|
+
var import_types29 = require("@saasicat/types");
|
|
14085
14858
|
function _ts_decorate85(decorators, target, key, desc) {
|
|
14086
14859
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
14087
14860
|
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
@@ -14120,15 +14893,31 @@ var StaticFeatureGuard = class {
|
|
|
14120
14893
|
if (!required || required.length === 0) return true;
|
|
14121
14894
|
const request = context.switchToHttp().getRequest();
|
|
14122
14895
|
const user = request.user;
|
|
14123
|
-
if (!user)
|
|
14896
|
+
if (!user) {
|
|
14897
|
+
throw new import_common75.ForbiddenException({
|
|
14898
|
+
code: import_types29.AUTH_ERROR_CODES.NOT_AUTHENTICATED,
|
|
14899
|
+
message: "Not authenticated"
|
|
14900
|
+
});
|
|
14901
|
+
}
|
|
14124
14902
|
const role = this.config?.userRoleResolver ? this.config.userRoleResolver(user) : user.role ?? user.platformRole;
|
|
14125
14903
|
if (role === "SUPER_ADMIN") return true;
|
|
14126
14904
|
const tenantId = this.config?.tenantIdResolver ? this.config.tenantIdResolver(request) : request.tenantId ?? user.tenantId;
|
|
14127
|
-
if (!tenantId)
|
|
14905
|
+
if (!tenantId) {
|
|
14906
|
+
throw new import_common75.ForbiddenException({
|
|
14907
|
+
code: import_types29.AUTH_ERROR_CODES.NO_TENANT_ASSIGNED,
|
|
14908
|
+
message: "No tenant assigned"
|
|
14909
|
+
});
|
|
14910
|
+
}
|
|
14128
14911
|
const snap = await this.entitlements.snapshot(tenantId);
|
|
14129
14912
|
const allowed = required.some((f) => snap.features.includes(f));
|
|
14130
14913
|
if (!allowed) {
|
|
14131
|
-
throw new import_common75.ForbiddenException(
|
|
14914
|
+
throw new import_common75.ForbiddenException({
|
|
14915
|
+
code: import_types29.FEATURE_NOT_LICENSED,
|
|
14916
|
+
message: `Feature ${required.join(" / ")} is not included in the current plan.`,
|
|
14917
|
+
params: {
|
|
14918
|
+
featureKeys: required
|
|
14919
|
+
}
|
|
14920
|
+
});
|
|
14132
14921
|
}
|
|
14133
14922
|
return true;
|
|
14134
14923
|
}
|
|
@@ -14371,7 +15160,7 @@ function buildTenantManifestController(options) {
|
|
|
14371
15160
|
async get(req) {
|
|
14372
15161
|
const tenantId = req.user?.tenantId ?? req.tenantId;
|
|
14373
15162
|
if (!tenantId) {
|
|
14374
|
-
throw new Error("
|
|
15163
|
+
throw new Error("No tenant on the request \u2014 TenantManifest requires user.tenantId.");
|
|
14375
15164
|
}
|
|
14376
15165
|
return this.svc.getManifest(tenantId);
|
|
14377
15166
|
}
|
|
@@ -14608,7 +15397,7 @@ var SaasPlatformModule = class _SaasPlatformModule {
|
|
|
14608
15397
|
throw new Error(`SaasPlatformModule.forRoot: entitlement active or required by the standard stack, but adapters are missing: ${missing.join(", ")}`);
|
|
14609
15398
|
}
|
|
14610
15399
|
if (persistence) {
|
|
14611
|
-
(0,
|
|
15400
|
+
(0, import_types30.assertPersistenceCapabilities)(persistence.capabilities, {
|
|
14612
15401
|
transactions: true,
|
|
14613
15402
|
pessimisticLocking: true
|
|
14614
15403
|
}, "SaasPlatformModule entitlement (transactional enforceLimit)");
|
|
@@ -15064,8 +15853,8 @@ var REGISTRATION_CONFIG_TOKEN = /* @__PURE__ */ Symbol.for("saas-platform/Regist
|
|
|
15064
15853
|
|
|
15065
15854
|
// src/registration/pending-registration.service.ts
|
|
15066
15855
|
var import_common80 = require("@nestjs/common");
|
|
15067
|
-
var
|
|
15068
|
-
var
|
|
15856
|
+
var import_types31 = require("@saasicat/types");
|
|
15857
|
+
var import_types32 = require("@saasicat/types");
|
|
15069
15858
|
|
|
15070
15859
|
// src/registration/pricing.ts
|
|
15071
15860
|
function round24(value) {
|
|
@@ -15183,7 +15972,7 @@ var PendingRegistrationService = class _PendingRegistrationService {
|
|
|
15183
15972
|
});
|
|
15184
15973
|
} catch (error) {
|
|
15185
15974
|
const message = error instanceof Error ? error.message : String(error);
|
|
15186
|
-
this.logger.warn(`Audit
|
|
15975
|
+
this.logger.warn(`Audit log write failed (${eventType}): ${message}`);
|
|
15187
15976
|
}
|
|
15188
15977
|
}
|
|
15189
15978
|
/** Public signup plan list for step 3 (frontend `usePublicPlans()`). */
|
|
@@ -15194,27 +15983,27 @@ var PendingRegistrationService = class _PendingRegistrationService {
|
|
|
15194
15983
|
const pending = await this.repo.findById(input.pendingRegistrationId);
|
|
15195
15984
|
if (!pending) {
|
|
15196
15985
|
throw new import_common80.NotFoundException({
|
|
15197
|
-
code:
|
|
15986
|
+
code: import_types31.REGISTRATION_ERROR_CODES.PENDING_REGISTRATION_NOT_FOUND
|
|
15198
15987
|
});
|
|
15199
15988
|
}
|
|
15200
15989
|
if (pending.expiresAt.getTime() < Date.now()) {
|
|
15201
15990
|
throw new import_common80.BadRequestException({
|
|
15202
|
-
code:
|
|
15991
|
+
code: import_types31.REGISTRATION_ERROR_CODES.PENDING_REGISTRATION_EXPIRED
|
|
15203
15992
|
});
|
|
15204
15993
|
}
|
|
15205
15994
|
if (pending.status !== "EMAIL_VERIFIED" && pending.status !== "PLAN_SELECTED" && pending.status !== "CHECKOUT_STARTED") {
|
|
15206
15995
|
throw new import_common80.BadRequestException({
|
|
15207
|
-
code:
|
|
15996
|
+
code: import_types31.REGISTRATION_ERROR_CODES.INVALID_REGISTRATION_STATE
|
|
15208
15997
|
});
|
|
15209
15998
|
}
|
|
15210
15999
|
const plan = await this.planCatalog.findPublicSignupPlan(input.planId);
|
|
15211
16000
|
if (!plan) {
|
|
15212
16001
|
throw new import_common80.BadRequestException({
|
|
15213
|
-
code:
|
|
16002
|
+
code: import_types31.REGISTRATION_ERROR_CODES.PLAN_NOT_AVAILABLE
|
|
15214
16003
|
});
|
|
15215
16004
|
}
|
|
15216
16005
|
const now = /* @__PURE__ */ new Date();
|
|
15217
|
-
const expiresAt = new Date(now.getTime() +
|
|
16006
|
+
const expiresAt = new Date(now.getTime() + import_types32.PENDING_ONBOARDING_TTL_DAYS * 24 * 60 * 60 * 1e3);
|
|
15218
16007
|
const updated = await this.repo.update(pending.id, {
|
|
15219
16008
|
status: "PLAN_SELECTED",
|
|
15220
16009
|
currentStep: 4,
|
|
@@ -15227,7 +16016,7 @@ var PendingRegistrationService = class _PendingRegistrationService {
|
|
|
15227
16016
|
return {
|
|
15228
16017
|
pendingRegistrationId: updated.id,
|
|
15229
16018
|
status: updated.status,
|
|
15230
|
-
nextStep:
|
|
16019
|
+
nextStep: import_types32.REGISTRATION_STEP_BY_STATUS[updated.status],
|
|
15231
16020
|
selectedPlanId: updated.selectedPlanId ?? plan.id
|
|
15232
16021
|
};
|
|
15233
16022
|
}
|
|
@@ -15235,28 +16024,28 @@ var PendingRegistrationService = class _PendingRegistrationService {
|
|
|
15235
16024
|
const pending = await this.repo.findById(input.pendingRegistrationId);
|
|
15236
16025
|
if (!pending) {
|
|
15237
16026
|
throw new import_common80.NotFoundException({
|
|
15238
|
-
code:
|
|
16027
|
+
code: import_types31.REGISTRATION_ERROR_CODES.PENDING_REGISTRATION_NOT_FOUND
|
|
15239
16028
|
});
|
|
15240
16029
|
}
|
|
15241
16030
|
if (pending.expiresAt.getTime() < Date.now()) {
|
|
15242
16031
|
throw new import_common80.BadRequestException({
|
|
15243
|
-
code:
|
|
16032
|
+
code: import_types31.REGISTRATION_ERROR_CODES.PENDING_REGISTRATION_EXPIRED
|
|
15244
16033
|
});
|
|
15245
16034
|
}
|
|
15246
16035
|
if (pending.status !== "PLAN_SELECTED" && pending.status !== "CHECKOUT_STARTED") {
|
|
15247
16036
|
throw new import_common80.BadRequestException({
|
|
15248
|
-
code:
|
|
16037
|
+
code: import_types31.REGISTRATION_ERROR_CODES.PLAN_NOT_SELECTED
|
|
15249
16038
|
});
|
|
15250
16039
|
}
|
|
15251
16040
|
if (!pending.selectedPlanId) {
|
|
15252
16041
|
throw new import_common80.BadRequestException({
|
|
15253
|
-
code:
|
|
16042
|
+
code: import_types31.REGISTRATION_ERROR_CODES.PLAN_NOT_SELECTED
|
|
15254
16043
|
});
|
|
15255
16044
|
}
|
|
15256
16045
|
const plan = await this.planCatalog.findPublicSignupPlan(pending.selectedPlanId);
|
|
15257
16046
|
if (!plan) {
|
|
15258
16047
|
throw new import_common80.BadRequestException({
|
|
15259
|
-
code:
|
|
16048
|
+
code: import_types31.REGISTRATION_ERROR_CODES.PLAN_NOT_AVAILABLE
|
|
15260
16049
|
});
|
|
15261
16050
|
}
|
|
15262
16051
|
const session = await this.paymentProvider.createCheckoutSession({
|
|
@@ -15267,7 +16056,7 @@ var PendingRegistrationService = class _PendingRegistrationService {
|
|
|
15267
16056
|
cancelUrl: input.cancelUrl
|
|
15268
16057
|
});
|
|
15269
16058
|
const now = /* @__PURE__ */ new Date();
|
|
15270
|
-
const expiresAt = new Date(now.getTime() +
|
|
16059
|
+
const expiresAt = new Date(now.getTime() + import_types32.PENDING_CHECKOUT_TTL_DAYS * 24 * 60 * 60 * 1e3);
|
|
15271
16060
|
const updated = await this.repo.update(pending.id, {
|
|
15272
16061
|
status: "CHECKOUT_STARTED",
|
|
15273
16062
|
currentStep: 4,
|
|
@@ -15282,7 +16071,7 @@ var PendingRegistrationService = class _PendingRegistrationService {
|
|
|
15282
16071
|
return {
|
|
15283
16072
|
pendingRegistrationId: updated.id,
|
|
15284
16073
|
status: updated.status,
|
|
15285
|
-
nextStep:
|
|
16074
|
+
nextStep: import_types32.REGISTRATION_STEP_BY_STATUS[updated.status],
|
|
15286
16075
|
checkoutSessionId: session.sessionId,
|
|
15287
16076
|
checkoutUrl: session.checkoutUrl
|
|
15288
16077
|
};
|
|
@@ -15308,7 +16097,7 @@ var PendingRegistrationService = class _PendingRegistrationService {
|
|
|
15308
16097
|
rawPayload: input.rawPayload
|
|
15309
16098
|
});
|
|
15310
16099
|
if (!claimed) {
|
|
15311
|
-
this.logger.warn(`
|
|
16100
|
+
this.logger.warn(`Idempotency: payment event ${input.eventId} already processed \u2014 duplicate discarded.`);
|
|
15312
16101
|
await this.record("PAYMENT_DUPLICATE_IGNORED", null, context, {
|
|
15313
16102
|
eventId: input.eventId,
|
|
15314
16103
|
sessionId: input.sessionId
|
|
@@ -15358,7 +16147,7 @@ var PendingRegistrationService = class _PendingRegistrationService {
|
|
|
15358
16147
|
tenantId: result.tenantId,
|
|
15359
16148
|
subscriptionId: result.subscriptionId
|
|
15360
16149
|
});
|
|
15361
|
-
this.logger.log(`
|
|
16150
|
+
this.logger.log(`Activation succeeded: pending=${pending.id} \u2192 user=${result.userId} tenant=${result.tenantId} subscription=${result.subscriptionId}`);
|
|
15362
16151
|
return {
|
|
15363
16152
|
activated: true,
|
|
15364
16153
|
result
|
|
@@ -15386,11 +16175,11 @@ var PendingRegistrationService = class _PendingRegistrationService {
|
|
|
15386
16175
|
await this.repo.delete(pending.id);
|
|
15387
16176
|
} catch (error) {
|
|
15388
16177
|
const message = error instanceof Error ? error.message : String(error);
|
|
15389
|
-
this.logger.warn(`Cleanup:
|
|
16178
|
+
this.logger.warn(`Cleanup: pending registration ${pending.id} could not be deleted (${message}).`);
|
|
15390
16179
|
}
|
|
15391
16180
|
}
|
|
15392
16181
|
if (expired.length > 0) {
|
|
15393
|
-
this.logger.log(`Cleanup: ${expired.length}
|
|
16182
|
+
this.logger.log(`Cleanup: ${expired.length} expired PendingRegistrations deleted.`);
|
|
15394
16183
|
}
|
|
15395
16184
|
return {
|
|
15396
16185
|
deleted: expired.length,
|
|
@@ -15400,7 +16189,7 @@ var PendingRegistrationService = class _PendingRegistrationService {
|
|
|
15400
16189
|
async start(input, context) {
|
|
15401
16190
|
const email = normalizeEmail(input.email);
|
|
15402
16191
|
if (await this.userLookup.hasActiveUser(email)) {
|
|
15403
|
-
this.logger.log(`register/start
|
|
16192
|
+
this.logger.log(`register/start ignored for an active account (${email}).`);
|
|
15404
16193
|
await this.record("REGISTRATION_NEUTRAL_ACTIVE_USER", null, context);
|
|
15405
16194
|
return {
|
|
15406
16195
|
neutral: true
|
|
@@ -15438,8 +16227,8 @@ var PendingRegistrationService = class _PendingRegistrationService {
|
|
|
15438
16227
|
const code = generateOtpCode();
|
|
15439
16228
|
const otpHash = hashOtpCode(code);
|
|
15440
16229
|
const now = /* @__PURE__ */ new Date();
|
|
15441
|
-
const otpExpiresAt = new Date(now.getTime() +
|
|
15442
|
-
const expiresAt = new Date(now.getTime() +
|
|
16230
|
+
const otpExpiresAt = new Date(now.getTime() + import_types32.OTP_TTL_MINUTES * 60 * 1e3);
|
|
16231
|
+
const expiresAt = new Date(now.getTime() + import_types32.PENDING_EMAIL_TTL_HOURS * 60 * 60 * 1e3);
|
|
15443
16232
|
const pending = await this.repo.create({
|
|
15444
16233
|
tenantName: input.tenantName.trim(),
|
|
15445
16234
|
tenantSlug: slug,
|
|
@@ -15467,13 +16256,13 @@ var PendingRegistrationService = class _PendingRegistrationService {
|
|
|
15467
16256
|
reason: "unknown_email"
|
|
15468
16257
|
});
|
|
15469
16258
|
throw new import_common80.BadRequestException({
|
|
15470
|
-
code:
|
|
16259
|
+
code: import_types31.REGISTRATION_ERROR_CODES.OTP_INVALID
|
|
15471
16260
|
});
|
|
15472
16261
|
}
|
|
15473
16262
|
if (pending.status !== "PENDING_EMAIL_VERIFICATION") {
|
|
15474
16263
|
return {
|
|
15475
16264
|
status: pending.status,
|
|
15476
|
-
nextStep:
|
|
16265
|
+
nextStep: import_types32.REGISTRATION_STEP_BY_STATUS[pending.status],
|
|
15477
16266
|
pendingRegistrationId: pending.id
|
|
15478
16267
|
};
|
|
15479
16268
|
}
|
|
@@ -15482,7 +16271,7 @@ var PendingRegistrationService = class _PendingRegistrationService {
|
|
|
15482
16271
|
reason: "no_otp_set"
|
|
15483
16272
|
});
|
|
15484
16273
|
throw new import_common80.BadRequestException({
|
|
15485
|
-
code:
|
|
16274
|
+
code: import_types31.REGISTRATION_ERROR_CODES.OTP_INVALID
|
|
15486
16275
|
});
|
|
15487
16276
|
}
|
|
15488
16277
|
if (pending.otpExpiresAt.getTime() < Date.now()) {
|
|
@@ -15490,7 +16279,7 @@ var PendingRegistrationService = class _PendingRegistrationService {
|
|
|
15490
16279
|
reason: "expired"
|
|
15491
16280
|
});
|
|
15492
16281
|
throw new import_common80.BadRequestException({
|
|
15493
|
-
code:
|
|
16282
|
+
code: import_types31.REGISTRATION_ERROR_CODES.OTP_EXPIRED
|
|
15494
16283
|
});
|
|
15495
16284
|
}
|
|
15496
16285
|
const maxAttempts = resolveOtpVerifyMaxAttempts();
|
|
@@ -15499,7 +16288,7 @@ var PendingRegistrationService = class _PendingRegistrationService {
|
|
|
15499
16288
|
reason: "locked"
|
|
15500
16289
|
});
|
|
15501
16290
|
throw new import_common80.BadRequestException({
|
|
15502
|
-
code:
|
|
16291
|
+
code: import_types31.REGISTRATION_ERROR_CODES.OTP_LOCKED
|
|
15503
16292
|
});
|
|
15504
16293
|
}
|
|
15505
16294
|
const attemptCount = await this.repo.incrementOtpAttemptCount(pending.id);
|
|
@@ -15508,7 +16297,7 @@ var PendingRegistrationService = class _PendingRegistrationService {
|
|
|
15508
16297
|
reason: "locked"
|
|
15509
16298
|
});
|
|
15510
16299
|
throw new import_common80.BadRequestException({
|
|
15511
|
-
code:
|
|
16300
|
+
code: import_types31.REGISTRATION_ERROR_CODES.OTP_LOCKED
|
|
15512
16301
|
});
|
|
15513
16302
|
}
|
|
15514
16303
|
if (!verifyOtpCode(pending.otpHash, code)) {
|
|
@@ -15516,7 +16305,7 @@ var PendingRegistrationService = class _PendingRegistrationService {
|
|
|
15516
16305
|
reason: "wrong_code"
|
|
15517
16306
|
});
|
|
15518
16307
|
throw new import_common80.BadRequestException({
|
|
15519
|
-
code:
|
|
16308
|
+
code: import_types31.REGISTRATION_ERROR_CODES.OTP_INVALID
|
|
15520
16309
|
});
|
|
15521
16310
|
}
|
|
15522
16311
|
const now = /* @__PURE__ */ new Date();
|
|
@@ -15526,12 +16315,12 @@ var PendingRegistrationService = class _PendingRegistrationService {
|
|
|
15526
16315
|
emailVerifiedAt: now,
|
|
15527
16316
|
otpHash: null,
|
|
15528
16317
|
otpExpiresAt: null,
|
|
15529
|
-
expiresAt: new Date(now.getTime() +
|
|
16318
|
+
expiresAt: new Date(now.getTime() + import_types32.PENDING_ONBOARDING_TTL_DAYS * 24 * 60 * 60 * 1e3)
|
|
15530
16319
|
});
|
|
15531
16320
|
await this.record("OTP_VERIFIED", updated.id, context);
|
|
15532
16321
|
return {
|
|
15533
16322
|
status: updated.status,
|
|
15534
|
-
nextStep:
|
|
16323
|
+
nextStep: import_types32.REGISTRATION_STEP_BY_STATUS[updated.status],
|
|
15535
16324
|
pendingRegistrationId: updated.id
|
|
15536
16325
|
};
|
|
15537
16326
|
}
|
|
@@ -15556,15 +16345,15 @@ var PendingRegistrationService = class _PendingRegistrationService {
|
|
|
15556
16345
|
}
|
|
15557
16346
|
async regenerateOtp(pending) {
|
|
15558
16347
|
const now = /* @__PURE__ */ new Date();
|
|
15559
|
-
const windowMs =
|
|
16348
|
+
const windowMs = import_types32.OTP_RATE_LIMIT_WINDOW_MINUTES * 60 * 1e3;
|
|
15560
16349
|
const withinWindow = pending.lastOtpSentAt !== null && now.getTime() - pending.lastOtpSentAt.getTime() < windowMs;
|
|
15561
|
-
if (withinWindow && pending.otpSendCount >=
|
|
15562
|
-
this.logger.warn(`OTP
|
|
16350
|
+
if (withinWindow && pending.otpSendCount >= import_types32.OTP_RATE_LIMIT_MAX_SENDS) {
|
|
16351
|
+
this.logger.warn(`OTP rate limit reached for ${pending.email} \u2014 send is being skipped.`);
|
|
15563
16352
|
return;
|
|
15564
16353
|
}
|
|
15565
16354
|
const code = generateOtpCode();
|
|
15566
16355
|
const otpHash = hashOtpCode(code);
|
|
15567
|
-
const otpExpiresAt = new Date(now.getTime() +
|
|
16356
|
+
const otpExpiresAt = new Date(now.getTime() + import_types32.OTP_TTL_MINUTES * 60 * 1e3);
|
|
15568
16357
|
const otpSendCount = withinWindow ? pending.otpSendCount + 1 : 1;
|
|
15569
16358
|
await this.repo.update(pending.id, {
|
|
15570
16359
|
otpHash,
|
|
@@ -15587,7 +16376,7 @@ var PendingRegistrationService = class _PendingRegistrationService {
|
|
|
15587
16376
|
});
|
|
15588
16377
|
} catch (error) {
|
|
15589
16378
|
const message = error instanceof Error ? error.message : String(error);
|
|
15590
|
-
this.logger.error(`OTP
|
|
16379
|
+
this.logger.error(`OTP email to ${pending.email} failed: ${message}`);
|
|
15591
16380
|
}
|
|
15592
16381
|
}
|
|
15593
16382
|
/**
|
|
@@ -15601,7 +16390,7 @@ var PendingRegistrationService = class _PendingRegistrationService {
|
|
|
15601
16390
|
}
|
|
15602
16391
|
const token = await this.resumeTokenSigner.sign({
|
|
15603
16392
|
pendingRegistrationId: pending.id,
|
|
15604
|
-
ttlMinutes:
|
|
16393
|
+
ttlMinutes: import_types31.REGISTRATION_RESUME_TTL_MINUTES
|
|
15605
16394
|
});
|
|
15606
16395
|
const base = (this.resumeBaseUrl ?? "http://localhost").replace(/\/+$/, "");
|
|
15607
16396
|
const url = `${base}/login?resume=${encodeURIComponent(token)}`;
|
|
@@ -15614,7 +16403,7 @@ var PendingRegistrationService = class _PendingRegistrationService {
|
|
|
15614
16403
|
});
|
|
15615
16404
|
} catch (error) {
|
|
15616
16405
|
const message = error instanceof Error ? error.message : String(error);
|
|
15617
|
-
this.logger.error(`Resume
|
|
16406
|
+
this.logger.error(`Resume email to ${pending.email} failed: ${message}`);
|
|
15618
16407
|
}
|
|
15619
16408
|
}
|
|
15620
16409
|
/**
|
|
@@ -15629,7 +16418,7 @@ var PendingRegistrationService = class _PendingRegistrationService {
|
|
|
15629
16418
|
async resumeWithToken(input, context) {
|
|
15630
16419
|
if (!this.resumeTokenSigner) {
|
|
15631
16420
|
throw new import_common80.BadRequestException({
|
|
15632
|
-
code:
|
|
16421
|
+
code: import_types31.REGISTRATION_ERROR_CODES.RESUME_NOT_CONFIGURED
|
|
15633
16422
|
});
|
|
15634
16423
|
}
|
|
15635
16424
|
let decoded;
|
|
@@ -15637,18 +16426,18 @@ var PendingRegistrationService = class _PendingRegistrationService {
|
|
|
15637
16426
|
decoded = await this.resumeTokenSigner.verify(input.token);
|
|
15638
16427
|
} catch {
|
|
15639
16428
|
throw new import_common80.BadRequestException({
|
|
15640
|
-
code:
|
|
16429
|
+
code: import_types31.REGISTRATION_ERROR_CODES.RESUME_TOKEN_INVALID
|
|
15641
16430
|
});
|
|
15642
16431
|
}
|
|
15643
16432
|
const pending = await this.repo.findById(decoded.pendingRegistrationId);
|
|
15644
16433
|
if (!pending) {
|
|
15645
16434
|
throw new import_common80.BadRequestException({
|
|
15646
|
-
code:
|
|
16435
|
+
code: import_types31.REGISTRATION_ERROR_CODES.RESUME_TOKEN_INVALID
|
|
15647
16436
|
});
|
|
15648
16437
|
}
|
|
15649
16438
|
if (pending.expiresAt.getTime() < Date.now()) {
|
|
15650
16439
|
throw new import_common80.BadRequestException({
|
|
15651
|
-
code:
|
|
16440
|
+
code: import_types31.REGISTRATION_ERROR_CODES.PENDING_REGISTRATION_EXPIRED
|
|
15652
16441
|
});
|
|
15653
16442
|
}
|
|
15654
16443
|
await this.record("REGISTRATION_NEUTRAL_REPLAY", pending.id, context, {
|
|
@@ -15658,7 +16447,7 @@ var PendingRegistrationService = class _PendingRegistrationService {
|
|
|
15658
16447
|
return {
|
|
15659
16448
|
pendingRegistrationId: pending.id,
|
|
15660
16449
|
status: pending.status,
|
|
15661
|
-
nextStep:
|
|
16450
|
+
nextStep: import_types32.REGISTRATION_STEP_BY_STATUS[pending.status],
|
|
15662
16451
|
snapshot: toSnapshot2(pending)
|
|
15663
16452
|
};
|
|
15664
16453
|
}
|
|
@@ -15678,7 +16467,7 @@ var PendingRegistrationService = class _PendingRegistrationService {
|
|
|
15678
16467
|
async getConfiguratorCatalog() {
|
|
15679
16468
|
if (!this.configuratorLookup) {
|
|
15680
16469
|
throw new import_common80.BadRequestException({
|
|
15681
|
-
code:
|
|
16470
|
+
code: import_types31.REGISTRATION_ERROR_CODES.CONFIGURATOR_NOT_CONFIGURED
|
|
15682
16471
|
});
|
|
15683
16472
|
}
|
|
15684
16473
|
return this.configuratorLookup.getCatalog();
|
|
@@ -15697,30 +16486,30 @@ var PendingRegistrationService = class _PendingRegistrationService {
|
|
|
15697
16486
|
async saveConfiguration(input, context) {
|
|
15698
16487
|
if (!this.configuratorLookup) {
|
|
15699
16488
|
throw new import_common80.BadRequestException({
|
|
15700
|
-
code:
|
|
16489
|
+
code: import_types31.REGISTRATION_ERROR_CODES.CONFIGURATOR_NOT_CONFIGURED
|
|
15701
16490
|
});
|
|
15702
16491
|
}
|
|
15703
16492
|
const pending = await this.repo.findById(input.pendingRegistrationId);
|
|
15704
16493
|
if (!pending) {
|
|
15705
16494
|
throw new import_common80.NotFoundException({
|
|
15706
|
-
code:
|
|
16495
|
+
code: import_types31.REGISTRATION_ERROR_CODES.PENDING_REGISTRATION_NOT_FOUND
|
|
15707
16496
|
});
|
|
15708
16497
|
}
|
|
15709
16498
|
if (pending.expiresAt.getTime() < Date.now()) {
|
|
15710
16499
|
throw new import_common80.BadRequestException({
|
|
15711
|
-
code:
|
|
16500
|
+
code: import_types31.REGISTRATION_ERROR_CODES.PENDING_REGISTRATION_EXPIRED
|
|
15712
16501
|
});
|
|
15713
16502
|
}
|
|
15714
16503
|
if (pending.status !== "EMAIL_VERIFIED" && pending.status !== "PLAN_SELECTED" && pending.status !== "CHECKOUT_STARTED") {
|
|
15715
16504
|
throw new import_common80.BadRequestException({
|
|
15716
|
-
code:
|
|
16505
|
+
code: import_types31.REGISTRATION_ERROR_CODES.INVALID_REGISTRATION_STATE
|
|
15717
16506
|
});
|
|
15718
16507
|
}
|
|
15719
16508
|
const catalog = await this.configuratorLookup.getCatalog();
|
|
15720
16509
|
const model = catalog.models.find((m) => m.id === input.selection.modelId);
|
|
15721
16510
|
if (!model) {
|
|
15722
16511
|
throw new import_common80.BadRequestException({
|
|
15723
|
-
code:
|
|
16512
|
+
code: import_types31.REGISTRATION_ERROR_CODES.MODEL_NOT_AVAILABLE
|
|
15724
16513
|
});
|
|
15725
16514
|
}
|
|
15726
16515
|
let promoEval;
|
|
@@ -15753,7 +16542,7 @@ var PendingRegistrationService = class _PendingRegistrationService {
|
|
|
15753
16542
|
};
|
|
15754
16543
|
}
|
|
15755
16544
|
const now = /* @__PURE__ */ new Date();
|
|
15756
|
-
const expiresAt = new Date(now.getTime() +
|
|
16545
|
+
const expiresAt = new Date(now.getTime() + import_types32.PENDING_ONBOARDING_TTL_DAYS * 24 * 60 * 60 * 1e3);
|
|
15757
16546
|
const updated = await this.repo.update(pending.id, {
|
|
15758
16547
|
status: "PLAN_SELECTED",
|
|
15759
16548
|
currentStep: 4,
|
|
@@ -15772,7 +16561,7 @@ var PendingRegistrationService = class _PendingRegistrationService {
|
|
|
15772
16561
|
return {
|
|
15773
16562
|
pendingRegistrationId: updated.id,
|
|
15774
16563
|
status: updated.status,
|
|
15775
|
-
nextStep:
|
|
16564
|
+
nextStep: import_types32.REGISTRATION_STEP_BY_STATUS[updated.status],
|
|
15776
16565
|
selection: input.selection,
|
|
15777
16566
|
breakdown
|
|
15778
16567
|
};
|
|
@@ -15786,26 +16575,26 @@ var PendingRegistrationService = class _PendingRegistrationService {
|
|
|
15786
16575
|
async previewConfigPromo(pendingRegistrationId, code) {
|
|
15787
16576
|
if (!this.configuratorLookup) {
|
|
15788
16577
|
throw new import_common80.BadRequestException({
|
|
15789
|
-
code:
|
|
16578
|
+
code: import_types31.REGISTRATION_ERROR_CODES.CONFIGURATOR_NOT_CONFIGURED
|
|
15790
16579
|
});
|
|
15791
16580
|
}
|
|
15792
16581
|
const pending = await this.repo.findById(pendingRegistrationId);
|
|
15793
16582
|
if (!pending) {
|
|
15794
16583
|
throw new import_common80.NotFoundException({
|
|
15795
|
-
code:
|
|
16584
|
+
code: import_types31.REGISTRATION_ERROR_CODES.PENDING_REGISTRATION_NOT_FOUND
|
|
15796
16585
|
});
|
|
15797
16586
|
}
|
|
15798
16587
|
const config = pending.configJson;
|
|
15799
16588
|
if (!config) {
|
|
15800
16589
|
throw new import_common80.BadRequestException({
|
|
15801
|
-
code:
|
|
16590
|
+
code: import_types31.REGISTRATION_ERROR_CODES.CONFIG_NOT_SAVED
|
|
15802
16591
|
});
|
|
15803
16592
|
}
|
|
15804
16593
|
const catalog = await this.configuratorLookup.getCatalog();
|
|
15805
16594
|
const model = catalog.models.find((m) => m.id === config.modelId);
|
|
15806
16595
|
if (!model) {
|
|
15807
16596
|
throw new import_common80.BadRequestException({
|
|
15808
|
-
code:
|
|
16597
|
+
code: import_types31.REGISTRATION_ERROR_CODES.MODEL_NOT_AVAILABLE
|
|
15809
16598
|
});
|
|
15810
16599
|
}
|
|
15811
16600
|
let promoEval;
|
|
@@ -15864,7 +16653,7 @@ var PendingRegistrationService = class _PendingRegistrationService {
|
|
|
15864
16653
|
return candidate;
|
|
15865
16654
|
}
|
|
15866
16655
|
}
|
|
15867
|
-
throw new Error(`
|
|
16656
|
+
throw new Error(`Could not find a free slug for "${tenantName}".`);
|
|
15868
16657
|
}
|
|
15869
16658
|
};
|
|
15870
16659
|
PendingRegistrationService = _ts_decorate90([
|
|
@@ -15914,9 +16703,9 @@ function normalizeEmail(value) {
|
|
|
15914
16703
|
__name(normalizeEmail, "normalizeEmail");
|
|
15915
16704
|
function resolveOtpVerifyMaxAttempts() {
|
|
15916
16705
|
const raw = process.env.SAAS_PLATFORM_OTP_VERIFY_MAX_ATTEMPTS;
|
|
15917
|
-
if (!raw) return
|
|
16706
|
+
if (!raw) return import_types32.OTP_VERIFY_MAX_ATTEMPTS;
|
|
15918
16707
|
const parsed = Number.parseInt(raw, 10);
|
|
15919
|
-
return Number.isInteger(parsed) && parsed > 0 ? parsed :
|
|
16708
|
+
return Number.isInteger(parsed) && parsed > 0 ? parsed : import_types32.OTP_VERIFY_MAX_ATTEMPTS;
|
|
15920
16709
|
}
|
|
15921
16710
|
__name(resolveOtpVerifyMaxAttempts, "resolveOtpVerifyMaxAttempts");
|
|
15922
16711
|
function toSnapshot2(pending) {
|
|
@@ -15989,7 +16778,7 @@ var RegistrationCleanupCron = class _RegistrationCleanupCron {
|
|
|
15989
16778
|
}
|
|
15990
16779
|
}
|
|
15991
16780
|
if (totalDeleted > 0) {
|
|
15992
|
-
this.logger.log(`
|
|
16781
|
+
this.logger.log(`Pending registration cleanup finished: ${totalDeleted} deleted in ${iterations} batches.`);
|
|
15993
16782
|
}
|
|
15994
16783
|
}
|
|
15995
16784
|
};
|
|
@@ -16011,6 +16800,7 @@ RegistrationCleanupCron = _ts_decorate91([
|
|
|
16011
16800
|
|
|
16012
16801
|
// src/registration/ip-rate-limit.guard.ts
|
|
16013
16802
|
var import_common82 = require("@nestjs/common");
|
|
16803
|
+
var import_types33 = require("@saasicat/types");
|
|
16014
16804
|
function _ts_decorate92(decorators, target, key, desc) {
|
|
16015
16805
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
16016
16806
|
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
@@ -16039,7 +16829,7 @@ var BaseIpRateLimitGuard = class {
|
|
|
16039
16829
|
const key = ipFingerprint2(req);
|
|
16040
16830
|
if (this.exceeded(key)) {
|
|
16041
16831
|
throw new import_common82.HttpException({
|
|
16042
|
-
code:
|
|
16832
|
+
code: import_types33.REGISTRATION_ERROR_CODES.RATE_LIMITED,
|
|
16043
16833
|
reason: this.options.name,
|
|
16044
16834
|
retryAfterSeconds: Math.ceil(this.options.windowMs / 1e3)
|
|
16045
16835
|
}, import_common82.HttpStatus.TOO_MANY_REQUESTS);
|
|
@@ -16183,7 +16973,7 @@ _ts_decorate94([
|
|
|
16183
16973
|
(0, import_class_validator17.IsOptional)(),
|
|
16184
16974
|
(0, import_class_validator17.IsString)(),
|
|
16185
16975
|
(0, import_class_validator17.Matches)(/^[a-z0-9-]+$/, {
|
|
16186
|
-
message: "tenantSlug
|
|
16976
|
+
message: "tenantSlug may only contain lower-case letters, digits and hyphens."
|
|
16187
16977
|
}),
|
|
16188
16978
|
(0, import_class_validator17.MaxLength)(60),
|
|
16189
16979
|
_ts_metadata73("design:type", String)
|
|
@@ -16219,7 +17009,7 @@ _ts_decorate94([
|
|
|
16219
17009
|
], RegisterStartDto.prototype, "password", void 0);
|
|
16220
17010
|
_ts_decorate94([
|
|
16221
17011
|
(0, import_class_validator17.Equals)(true, {
|
|
16222
|
-
message: "
|
|
17012
|
+
message: "The terms of service, privacy policy and data processing agreement must be accepted."
|
|
16223
17013
|
}),
|
|
16224
17014
|
_ts_metadata73("design:type", Boolean)
|
|
16225
17015
|
], RegisterStartDto.prototype, "termsAccepted", void 0);
|
|
@@ -16259,7 +17049,7 @@ _ts_decorate95([
|
|
|
16259
17049
|
_ts_decorate95([
|
|
16260
17050
|
(0, import_class_validator18.IsString)(),
|
|
16261
17051
|
(0, import_class_validator18.Matches)(/^\d{6}$/, {
|
|
16262
|
-
message: "OTP
|
|
17052
|
+
message: "The OTP must be 6 digits."
|
|
16263
17053
|
}),
|
|
16264
17054
|
_ts_metadata74("design:type", String)
|
|
16265
17055
|
], VerifyRegistrationOtpDto.prototype, "otp", void 0);
|
|
@@ -16790,8 +17580,8 @@ __export(src_exports, {
|
|
|
16790
17580
|
buildPublicMarketingCatalogController: () => buildPublicMarketingCatalogController,
|
|
16791
17581
|
buildReplacedByIndex: () => buildReplacedByIndex,
|
|
16792
17582
|
buildTenantSubscriptionBundlesController: () => buildTenantSubscriptionBundlesController,
|
|
16793
|
-
classifyBundleVersionDiff: () =>
|
|
16794
|
-
classifyPlanDiff: () =>
|
|
17583
|
+
classifyBundleVersionDiff: () => import_types4.classifyBundleVersionDiff,
|
|
17584
|
+
classifyPlanDiff: () => import_types4.classifyPlanDiff,
|
|
16795
17585
|
clearPendingPlanVersionFields: () => clearPendingPlanVersionFields,
|
|
16796
17586
|
collectSubscriptionBundleFeatures: () => collectSubscriptionBundleFeatures,
|
|
16797
17587
|
computeBreakdown: () => computeBreakdown,
|
|
@@ -16884,7 +17674,7 @@ __export(testing_exports, {
|
|
|
16884
17674
|
});
|
|
16885
17675
|
|
|
16886
17676
|
// src/testing/fake-repositories.ts
|
|
16887
|
-
var
|
|
17677
|
+
var import_types34 = require("@saasicat/types");
|
|
16888
17678
|
var FakeSubscriptionRepository = class {
|
|
16889
17679
|
static {
|
|
16890
17680
|
__name(this, "FakeSubscriptionRepository");
|
|
@@ -16975,10 +17765,10 @@ var FakeSubscriptionBundleRepository = class {
|
|
|
16975
17765
|
async cancel(subscriptionBundleId, data) {
|
|
16976
17766
|
const existing = this.byId.get(subscriptionBundleId);
|
|
16977
17767
|
if (!existing) {
|
|
16978
|
-
throw new Error(`SubscriptionBundle '${subscriptionBundleId}'
|
|
17768
|
+
throw new Error(`SubscriptionBundle '${subscriptionBundleId}' not found`);
|
|
16979
17769
|
}
|
|
16980
17770
|
if (existing.canceledAt !== null) {
|
|
16981
|
-
throw new Error(`SubscriptionBundle '${subscriptionBundleId}'
|
|
17771
|
+
throw new Error(`SubscriptionBundle '${subscriptionBundleId}' is already cancelled`);
|
|
16982
17772
|
}
|
|
16983
17773
|
const updated = {
|
|
16984
17774
|
...existing,
|
|
@@ -16992,7 +17782,7 @@ var FakeSubscriptionBundleRepository = class {
|
|
|
16992
17782
|
async reactivate(subscriptionBundleId) {
|
|
16993
17783
|
const existing = this.byId.get(subscriptionBundleId);
|
|
16994
17784
|
if (!existing) {
|
|
16995
|
-
throw new Error(`SubscriptionBundle '${subscriptionBundleId}'
|
|
17785
|
+
throw new Error(`SubscriptionBundle '${subscriptionBundleId}' not found`);
|
|
16996
17786
|
}
|
|
16997
17787
|
const updated = {
|
|
16998
17788
|
...existing,
|
|
@@ -17102,7 +17892,7 @@ var FakeSubscriptionContractRepository = class {
|
|
|
17102
17892
|
}
|
|
17103
17893
|
async terminate(contractId, data) {
|
|
17104
17894
|
const existing = this.byId.get(contractId);
|
|
17105
|
-
if (!existing) throw new Error(`SubscriptionContract '${contractId}'
|
|
17895
|
+
if (!existing) throw new Error(`SubscriptionContract '${contractId}' not found`);
|
|
17106
17896
|
const updated = {
|
|
17107
17897
|
...existing,
|
|
17108
17898
|
status: data.status,
|
|
@@ -17262,7 +18052,7 @@ var FakeBundleRepository = class {
|
|
|
17262
18052
|
}
|
|
17263
18053
|
async update(bundleId, data) {
|
|
17264
18054
|
const existing = this.bundles.get(bundleId);
|
|
17265
|
-
if (!existing) throw new Error(`Bundle '${bundleId}'
|
|
18055
|
+
if (!existing) throw new Error(`Bundle '${bundleId}' not found`);
|
|
17266
18056
|
const updated = {
|
|
17267
18057
|
...existing,
|
|
17268
18058
|
label: data.label ?? existing.label,
|
|
@@ -17277,7 +18067,7 @@ var FakeBundleRepository = class {
|
|
|
17277
18067
|
}
|
|
17278
18068
|
async softDelete(bundleId) {
|
|
17279
18069
|
const existing = this.bundles.get(bundleId);
|
|
17280
|
-
if (!existing) throw new Error(`Bundle '${bundleId}'
|
|
18070
|
+
if (!existing) throw new Error(`Bundle '${bundleId}' not found`);
|
|
17281
18071
|
this.bundles.set(bundleId, {
|
|
17282
18072
|
...existing,
|
|
17283
18073
|
deletedAt: this.nowIso()
|
|
@@ -17309,7 +18099,7 @@ var FakeBundleRepository = class {
|
|
|
17309
18099
|
async createDraft(data) {
|
|
17310
18100
|
const draft = await this.findCurrentDraft(data.bundleId);
|
|
17311
18101
|
if (draft) {
|
|
17312
|
-
throw new Error(`Bundle '${data.bundleId}'
|
|
18102
|
+
throw new Error(`Bundle '${data.bundleId}' already has a draft version v${draft.version}`);
|
|
17313
18103
|
}
|
|
17314
18104
|
const all = await this.listVersions(data.bundleId);
|
|
17315
18105
|
const nextVersion = all.length === 0 ? 1 : Math.max(...all.map((v) => v.version)) + 1;
|
|
@@ -17345,7 +18135,7 @@ var FakeBundleRepository = class {
|
|
|
17345
18135
|
}
|
|
17346
18136
|
async updateDraft(versionId, data) {
|
|
17347
18137
|
const existing = this.versions.get(versionId);
|
|
17348
|
-
if (!existing) throw new Error(`BundleVersion '${versionId}'
|
|
18138
|
+
if (!existing) throw new Error(`BundleVersion '${versionId}' not found`);
|
|
17349
18139
|
const updated = {
|
|
17350
18140
|
...existing,
|
|
17351
18141
|
features: data.features ?? existing.features,
|
|
@@ -17367,15 +18157,15 @@ var FakeBundleRepository = class {
|
|
|
17367
18157
|
const existing = this.versions.get(versionId);
|
|
17368
18158
|
if (!existing) return;
|
|
17369
18159
|
if (existing.publishedAt !== null) {
|
|
17370
|
-
throw new Error(`BundleVersion '${versionId}'
|
|
18160
|
+
throw new Error(`BundleVersion '${versionId}' is already published`);
|
|
17371
18161
|
}
|
|
17372
18162
|
this.versions.delete(versionId);
|
|
17373
18163
|
}
|
|
17374
18164
|
async publishDraft(versionId, publishMeta) {
|
|
17375
18165
|
const draft = this.versions.get(versionId);
|
|
17376
|
-
if (!draft) throw new Error(`BundleVersion '${versionId}'
|
|
18166
|
+
if (!draft) throw new Error(`BundleVersion '${versionId}' not found`);
|
|
17377
18167
|
if (draft.publishedAt !== null) {
|
|
17378
|
-
throw new Error(`BundleVersion '${versionId}'
|
|
18168
|
+
throw new Error(`BundleVersion '${versionId}' is already published`);
|
|
17379
18169
|
}
|
|
17380
18170
|
const now = this.nowIso();
|
|
17381
18171
|
const validFromIso = publishMeta.validFrom.toISOString();
|
|
@@ -17451,7 +18241,7 @@ var FakeMarketingProjectionRepository = class {
|
|
|
17451
18241
|
const locale = data.locale ?? "de";
|
|
17452
18242
|
const existing = await this.findByTarget(data.targetType, data.targetVersionId, locale);
|
|
17453
18243
|
if (existing) {
|
|
17454
|
-
throw new Error(`
|
|
18244
|
+
throw new Error(`Marketing projection for ${data.targetType}/${data.targetVersionId}/${locale} already exists`);
|
|
17455
18245
|
}
|
|
17456
18246
|
const now = this.nowIso();
|
|
17457
18247
|
const row = {
|
|
@@ -17479,7 +18269,7 @@ var FakeMarketingProjectionRepository = class {
|
|
|
17479
18269
|
}
|
|
17480
18270
|
async update(id, data) {
|
|
17481
18271
|
const existing = this.rows.get(id);
|
|
17482
|
-
if (!existing) throw new Error(`MarketingProjection '${id}'
|
|
18272
|
+
if (!existing) throw new Error(`MarketingProjection '${id}' not found`);
|
|
17483
18273
|
const updated = {
|
|
17484
18274
|
...existing,
|
|
17485
18275
|
displayLabel: data.displayLabel ?? existing.displayLabel,
|
|
@@ -17499,7 +18289,7 @@ var FakeMarketingProjectionRepository = class {
|
|
|
17499
18289
|
return updated;
|
|
17500
18290
|
}
|
|
17501
18291
|
async delete(id) {
|
|
17502
|
-
if (!this.rows.has(id)) throw new Error(`MarketingProjection '${id}'
|
|
18292
|
+
if (!this.rows.has(id)) throw new Error(`MarketingProjection '${id}' not found`);
|
|
17503
18293
|
this.rows.delete(id);
|
|
17504
18294
|
}
|
|
17505
18295
|
};
|
|
@@ -17573,7 +18363,7 @@ var FakePlanRepository = class {
|
|
|
17573
18363
|
}
|
|
17574
18364
|
async update(planId, data) {
|
|
17575
18365
|
const existing = this.plans.get(planId);
|
|
17576
|
-
if (!existing) throw new Error(`Plan '${planId}'
|
|
18366
|
+
if (!existing) throw new Error(`Plan '${planId}' not found`);
|
|
17577
18367
|
const updated = {
|
|
17578
18368
|
...existing,
|
|
17579
18369
|
label: data.label ?? existing.label,
|
|
@@ -17587,7 +18377,7 @@ var FakePlanRepository = class {
|
|
|
17587
18377
|
}
|
|
17588
18378
|
async softDelete(planId) {
|
|
17589
18379
|
const existing = this.plans.get(planId);
|
|
17590
|
-
if (!existing) throw new Error(`Plan '${planId}'
|
|
18380
|
+
if (!existing) throw new Error(`Plan '${planId}' not found`);
|
|
17591
18381
|
const updated = {
|
|
17592
18382
|
...existing,
|
|
17593
18383
|
deletedAt: this.nowIso(),
|
|
@@ -17640,7 +18430,7 @@ var FakePlanRepository = class {
|
|
|
17640
18430
|
}
|
|
17641
18431
|
async findActivePlanVersion(planKey, asOf = /* @__PURE__ */ new Date()) {
|
|
17642
18432
|
const t = asOf.getTime();
|
|
17643
|
-
const dayStart = (0,
|
|
18433
|
+
const dayStart = (0, import_types34.startOfUtcDay)(asOf).getTime();
|
|
17644
18434
|
const matches = [
|
|
17645
18435
|
...this.versions.values()
|
|
17646
18436
|
].filter((v) => {
|
|
@@ -17704,7 +18494,7 @@ var FakePlanRepository = class {
|
|
|
17704
18494
|
}
|
|
17705
18495
|
async updatePlanVersionDraft(versionId, data) {
|
|
17706
18496
|
const existing = this.versions.get(versionId);
|
|
17707
|
-
if (!existing) throw new Error(`PlanVersion '${versionId}'
|
|
18497
|
+
if (!existing) throw new Error(`PlanVersion '${versionId}' not found`);
|
|
17708
18498
|
const updated = {
|
|
17709
18499
|
...existing,
|
|
17710
18500
|
features: data.features ?? existing.features,
|
|
@@ -17721,7 +18511,7 @@ var FakePlanRepository = class {
|
|
|
17721
18511
|
}
|
|
17722
18512
|
async publishPlanVersionDraft(versionId, publishMeta) {
|
|
17723
18513
|
const draft = this.versions.get(versionId);
|
|
17724
|
-
if (!draft) throw new Error(`PlanVersion '${versionId}'
|
|
18514
|
+
if (!draft) throw new Error(`PlanVersion '${versionId}' not found`);
|
|
17725
18515
|
const planKey = draft.planId;
|
|
17726
18516
|
const now = this.nowIso();
|
|
17727
18517
|
const dayMs = 24 * 60 * 60 * 1e3;
|
|
@@ -17753,14 +18543,14 @@ var FakePlanRepository = class {
|
|
|
17753
18543
|
const existing = this.versions.get(versionId);
|
|
17754
18544
|
if (!existing) return;
|
|
17755
18545
|
if (existing.publishedAt !== null) {
|
|
17756
|
-
throw new Error(`FakePlanRepository:
|
|
18546
|
+
throw new Error(`FakePlanRepository: plan version '${versionId}' is already published \u2014 discard is not allowed`);
|
|
17757
18547
|
}
|
|
17758
18548
|
this.versions.delete(versionId);
|
|
17759
18549
|
}
|
|
17760
18550
|
async terminate(versionId, endsAt) {
|
|
17761
18551
|
const existing = this.versions.get(versionId);
|
|
17762
18552
|
if (!existing) {
|
|
17763
|
-
throw new Error(`FakePlanRepository: PlanVersion '${versionId}'
|
|
18553
|
+
throw new Error(`FakePlanRepository: PlanVersion '${versionId}' not found`);
|
|
17764
18554
|
}
|
|
17765
18555
|
const updated = {
|
|
17766
18556
|
...existing,
|