@cosmicdrift/kumiko-bundled-features 0.316.0 → 0.318.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/package.json +10 -9
- package/src/auth-email-password/web/__tests__/use-url-token.test.tsx +1 -1
- package/src/billing-foundation/__tests__/billing-foundation.integration.test.ts +83 -4
- package/src/billing-foundation/__tests__/billing-plans-admin-only.integration.test.ts +158 -0
- package/src/billing-foundation/__tests__/billing-plans.integration.test.ts +800 -0
- package/src/billing-foundation/__tests__/checkout-core.test.ts +149 -0
- package/src/billing-foundation/__tests__/constants.test.ts +72 -0
- package/src/billing-foundation/__tests__/feature.test.ts +119 -1
- package/src/billing-foundation/changes.json +18 -0
- package/src/billing-foundation/checkout-core.ts +259 -0
- package/src/billing-foundation/constants.ts +98 -0
- package/src/billing-foundation/feature.ts +155 -87
- package/src/billing-foundation/get-subscription-for-tenant.ts +5 -0
- package/src/billing-foundation/handlers/billing-plans.query.ts +32 -0
- package/src/billing-foundation/handlers/create-checkout-session.write.ts +42 -44
- package/src/billing-foundation/handlers/create-portal-session.write.ts +80 -59
- package/src/billing-foundation/handlers/start-plan-checkout.write.ts +75 -0
- package/src/billing-foundation/handlers/switch-plan.write.ts +115 -0
- package/src/billing-foundation/i18n.ts +62 -0
- package/src/billing-foundation/index.ts +16 -1
- package/src/billing-foundation/plan-catalog.ts +204 -0
- package/src/billing-foundation/projection.ts +17 -7
- package/src/billing-foundation/screens.ts +25 -0
- package/src/billing-foundation/types.ts +147 -1
- package/src/billing-foundation/validate-options.ts +66 -0
- package/src/billing-foundation/web/__tests__/billing-plans-panel.test.tsx +352 -0
- package/src/billing-foundation/web/billing-plans-panel.tsx +241 -0
- package/src/billing-foundation/web/client-plugin.tsx +21 -0
- package/src/billing-foundation/web/index.ts +8 -0
- package/src/cap-counter/__tests__/stock-cap-guard.integration.test.ts +88 -0
- package/src/cap-counter/__tests__/with-cap-enforcement.integration.test.ts +102 -0
- package/src/cap-counter/aggregate-id.ts +4 -17
- package/src/cap-counter/book-cap-usage.ts +73 -43
- package/src/cap-counter/changes.json +6 -0
- package/src/cap-counter/handlers/increment.write.ts +2 -3
- package/src/cap-counter/index.ts +1 -0
- package/src/cap-counter/stock-cap-guard.ts +17 -5
- package/src/cap-counter/with-cap-enforcement.ts +4 -2
- package/src/cap-overview/__tests__/cap-limit-from-config.integration.test.ts +141 -0
- package/src/cap-overview/changes.json +6 -0
- package/src/cap-overview/handlers/caps-usage.query.ts +1 -1
- package/src/cap-overview/handlers/tenant-caps-list.query.ts +26 -1
- package/src/cap-overview/types.ts +6 -1
- package/src/subscription-mollie/changes.json +8 -1
- package/src/subscription-mollie/feature.ts +1 -0
- package/src/subscription-stripe/__tests__/feature.test.ts +5 -1
- package/src/subscription-stripe/__tests__/plugin-methods.test.ts +517 -3
- package/src/subscription-stripe/__tests__/runtime.test.ts +35 -0
- package/src/subscription-stripe/__tests__/stripe-foundation.integration.test.ts +54 -1
- package/src/subscription-stripe/__tests__/verify-webhook.test.ts +2 -2
- package/src/subscription-stripe/changes.json +18 -0
- package/src/subscription-stripe/feature.ts +15 -1
- package/src/subscription-stripe/plugin-methods.ts +344 -1
- package/src/subscription-stripe/runtime.ts +23 -4
- package/src/subscription-stripe/verify-webhook.ts +7 -1
- package/src/tier-engine/tier-resolver.ts +4 -3
- package/src/user-data-rights/web/__tests__/privacy-center-screen.test.tsx +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-bundled-features",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.318.0",
|
|
4
4
|
"description": "Built-in features — tenant, user, auth, delivery. The stuff you'd rewrite anyway, already typed.",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
|
|
@@ -55,6 +55,7 @@
|
|
|
55
55
|
"./ledger": "./src/ledger/index.ts",
|
|
56
56
|
"./ledger/web": "./src/ledger/web/index.ts",
|
|
57
57
|
"./billing-foundation": "./src/billing-foundation/index.ts",
|
|
58
|
+
"./billing-foundation/web": "./src/billing-foundation/web/index.ts",
|
|
58
59
|
"./subscription-stripe": "./src/subscription-stripe/index.ts",
|
|
59
60
|
"./subscription-mollie": "./src/subscription-mollie/index.ts",
|
|
60
61
|
"./foundation-shared": "./src/foundation-shared/index.ts",
|
|
@@ -133,12 +134,12 @@
|
|
|
133
134
|
"./workflow-runner": "./src/workflow-runner/index.ts"
|
|
134
135
|
},
|
|
135
136
|
"dependencies": {
|
|
136
|
-
"@cosmicdrift/kumiko-dispatcher-live": "0.
|
|
137
|
-
"@cosmicdrift/kumiko-framework": "0.
|
|
138
|
-
"@cosmicdrift/kumiko-headless": "0.
|
|
139
|
-
"@cosmicdrift/kumiko-renderer": "0.
|
|
140
|
-
"@cosmicdrift/kumiko-renderer-web": "0.
|
|
141
|
-
"@cosmicdrift/kumiko-types": "0.
|
|
137
|
+
"@cosmicdrift/kumiko-dispatcher-live": "0.318.0",
|
|
138
|
+
"@cosmicdrift/kumiko-framework": "0.318.0",
|
|
139
|
+
"@cosmicdrift/kumiko-headless": "0.318.0",
|
|
140
|
+
"@cosmicdrift/kumiko-renderer": "0.318.0",
|
|
141
|
+
"@cosmicdrift/kumiko-renderer-web": "0.318.0",
|
|
142
|
+
"@cosmicdrift/kumiko-types": "0.318.0",
|
|
142
143
|
"@mollie/api-client": "^4.5.0",
|
|
143
144
|
"@node-rs/argon2": "^2.0.2",
|
|
144
145
|
"@types/mailparser": "^3.4.6",
|
|
@@ -167,8 +168,8 @@
|
|
|
167
168
|
],
|
|
168
169
|
"devDependencies": {
|
|
169
170
|
"@testing-library/user-event": "^14.6.1",
|
|
170
|
-
"@cosmicdrift/kumiko-locale-de": "0.
|
|
171
|
-
"@cosmicdrift/kumiko-locale-es": "0.
|
|
171
|
+
"@cosmicdrift/kumiko-locale-de": "0.318.0",
|
|
172
|
+
"@cosmicdrift/kumiko-locale-es": "0.318.0",
|
|
172
173
|
"jsqr": "^1.4.0"
|
|
173
174
|
}
|
|
174
175
|
}
|
|
@@ -4,7 +4,7 @@ import { useUrlToken } from "../auth-form-primitives";
|
|
|
4
4
|
|
|
5
5
|
// #774: magic-link tokens must not linger in browser history / Referer.
|
|
6
6
|
// useUrlToken reads the token once, then scrubs the param via replaceState.
|
|
7
|
-
// (dom
|
|
7
|
+
// (preload/dom resets window.location to http://localhost/ after each test.)
|
|
8
8
|
describe("useUrlToken (magic-link history hygiene)", () => {
|
|
9
9
|
test("reads ?token= and strips it from the URL, keeping other params", () => {
|
|
10
10
|
window.history.replaceState(null, "", "http://localhost/reset?token=secret-abc&keep=1");
|
|
@@ -47,7 +47,7 @@ import {
|
|
|
47
47
|
SubscriptionFoundationHandlers,
|
|
48
48
|
SubscriptionStatuses,
|
|
49
49
|
} from "../constants";
|
|
50
|
-
import {
|
|
50
|
+
import { createBillingFoundationFeature } from "../feature";
|
|
51
51
|
import { paymentsProjectionTable, subscriptionsProjectionTable } from "../projection";
|
|
52
52
|
import type { PaymentEvent, SubscriptionProviderPlugin } from "../types";
|
|
53
53
|
import { createSubscriptionWebhookRoute } from "../webhook-handler";
|
|
@@ -73,6 +73,10 @@ const mockProviderFeature = defineFeature("test-mock-provider", (r) => {
|
|
|
73
73
|
r.requires("billing-foundation");
|
|
74
74
|
const plugin: SubscriptionProviderPlugin = {
|
|
75
75
|
verifyAndParseWebhook: async () => null,
|
|
76
|
+
// Only the two tiers scenario 6's subscription-mode checkouts actually
|
|
77
|
+
// exercise — the hardened create-checkout-session now requires every
|
|
78
|
+
// mode:"subscription" priceId to resolve through priceToTier.
|
|
79
|
+
priceToTier: { price_pro_test: "pro", price_business_test: "business" },
|
|
76
80
|
createCheckoutSession: async (_ctx, options) => {
|
|
77
81
|
mockCheckoutCalls.push({
|
|
78
82
|
priceId: options.priceId,
|
|
@@ -131,7 +135,7 @@ beforeAll(async () => {
|
|
|
131
135
|
createTenantFeature(),
|
|
132
136
|
createComplianceProfilesFeature(),
|
|
133
137
|
createTenantLifecycleFeature(),
|
|
134
|
-
|
|
138
|
+
createBillingFoundationFeature({ baseUrl: "https://example.com" }),
|
|
135
139
|
mockProviderFeature,
|
|
136
140
|
mockPaymentProviderFeature,
|
|
137
141
|
],
|
|
@@ -480,7 +484,10 @@ describe("scenario 5: Provider-Wechsel mid-period (Disney+-Pattern)", () => {
|
|
|
480
484
|
describe("scenario 6: create-checkout-session — Plugin-routing", () => {
|
|
481
485
|
test("happy-path: valid provider → URL durchgereicht + plugin mit korrekten args aufgerufen", async () => {
|
|
482
486
|
mockCheckoutCalls.length = 0;
|
|
483
|
-
|
|
487
|
+
// Not 3009 — scenario 5 reuses that tenant number and leaves it with a
|
|
488
|
+
// non-terminal "pro" subscription; openCheckout's new existing-
|
|
489
|
+
// subscription conflict-gate would reject this checkout for it.
|
|
490
|
+
const admin = adminFor(3016);
|
|
484
491
|
const result = (await stack.http.writeOk(
|
|
485
492
|
"billing-foundation:write:create-checkout-session",
|
|
486
493
|
{
|
|
@@ -525,7 +532,26 @@ describe("scenario 6: create-checkout-session — Plugin-routing", () => {
|
|
|
525
532
|
|
|
526
533
|
test("optional providerCustomerId wird durchgereicht (Plan-Wechsel-Flow)", async () => {
|
|
527
534
|
mockCheckoutCalls.length = 0;
|
|
528
|
-
|
|
535
|
+
// Not 3012 — that tenant already gets its own subscription in the
|
|
536
|
+
// crypto-shredding test further down and would collide.
|
|
537
|
+
const admin = adminFor(3018);
|
|
538
|
+
|
|
539
|
+
// The provider customer-id must belong to the caller's own subscription
|
|
540
|
+
// (even if canceled) — otherwise the foreign_provider_customer guard fires.
|
|
541
|
+
await stack.http.writeOk(
|
|
542
|
+
SubscriptionFoundationHandlers.processEvent,
|
|
543
|
+
{
|
|
544
|
+
...buildEvent({
|
|
545
|
+
providerEventId: "evt_3018_create",
|
|
546
|
+
providerCustomerId: "cus_existing_xyz",
|
|
547
|
+
providerSubscriptionId: "sub_3018",
|
|
548
|
+
status: SubscriptionStatuses.canceled,
|
|
549
|
+
}),
|
|
550
|
+
providerName: "mock",
|
|
551
|
+
},
|
|
552
|
+
admin,
|
|
553
|
+
);
|
|
554
|
+
|
|
529
555
|
await stack.http.writeOk(
|
|
530
556
|
"billing-foundation:write:create-checkout-session",
|
|
531
557
|
{
|
|
@@ -540,6 +566,59 @@ describe("scenario 6: create-checkout-session — Plugin-routing", () => {
|
|
|
540
566
|
expect(mockCheckoutCalls[0]?.providerCustomerId).toBe("cus_existing_xyz");
|
|
541
567
|
});
|
|
542
568
|
|
|
569
|
+
test("fremde providerCustomerId wird abgelehnt (foreign_provider_customer)", async () => {
|
|
570
|
+
mockCheckoutCalls.length = 0;
|
|
571
|
+
const other = adminFor(3019);
|
|
572
|
+
await stack.http.writeOk(
|
|
573
|
+
SubscriptionFoundationHandlers.processEvent,
|
|
574
|
+
{
|
|
575
|
+
...buildEvent({
|
|
576
|
+
providerEventId: "evt_3019_create",
|
|
577
|
+
providerCustomerId: "cus_belongs_to_tenant_3019",
|
|
578
|
+
providerSubscriptionId: "sub_3019",
|
|
579
|
+
}),
|
|
580
|
+
providerName: "mock",
|
|
581
|
+
},
|
|
582
|
+
other,
|
|
583
|
+
);
|
|
584
|
+
|
|
585
|
+
const admin = adminFor(3017);
|
|
586
|
+
const error = await stack.http.writeErr(
|
|
587
|
+
"billing-foundation:write:create-checkout-session",
|
|
588
|
+
{
|
|
589
|
+
providerName: "mock",
|
|
590
|
+
priceId: "price_pro_test",
|
|
591
|
+
successUrl: "https://example.com/s",
|
|
592
|
+
cancelUrl: "https://example.com/c",
|
|
593
|
+
providerCustomerId: "cus_belongs_to_tenant_3019",
|
|
594
|
+
},
|
|
595
|
+
admin,
|
|
596
|
+
);
|
|
597
|
+
expect(error.httpStatus).toBe(422);
|
|
598
|
+
expect(error.i18nKey).toBe("billing-foundation.errors.foreignProviderCustomer");
|
|
599
|
+
expect(mockCheckoutCalls).toHaveLength(0);
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
test("fremde providerCustomerId wird auch im mode:payment abgelehnt", async () => {
|
|
603
|
+
mockCheckoutCalls.length = 0;
|
|
604
|
+
const admin = adminFor(3020);
|
|
605
|
+
const error = await stack.http.writeErr(
|
|
606
|
+
"billing-foundation:write:create-checkout-session",
|
|
607
|
+
{
|
|
608
|
+
providerName: "mock",
|
|
609
|
+
priceId: "price_topup_test",
|
|
610
|
+
successUrl: "https://example.com/s",
|
|
611
|
+
cancelUrl: "https://example.com/c",
|
|
612
|
+
providerCustomerId: "cus_belongs_to_tenant_3019",
|
|
613
|
+
mode: "payment",
|
|
614
|
+
},
|
|
615
|
+
admin,
|
|
616
|
+
);
|
|
617
|
+
expect(error.httpStatus).toBe(422);
|
|
618
|
+
expect(error.i18nKey).toBe("billing-foundation.errors.foreignProviderCustomer");
|
|
619
|
+
expect(mockCheckoutCalls).toHaveLength(0);
|
|
620
|
+
});
|
|
621
|
+
|
|
543
622
|
test("optional mode wird durchgereicht (One-off-Payment-Flow, fw#2755)", async () => {
|
|
544
623
|
mockCheckoutCalls.length = 0;
|
|
545
624
|
const admin = adminFor(3014);
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
// Admin-only billing-plans catalog — a separate stack (own viewRoles/
|
|
2
|
+
// purchaseRoles: ["Admin"]) instead of a describe-block inside
|
|
3
|
+
// billing-plans.integration.test.ts, matching how show-pony pins a distinct
|
|
4
|
+
// role-shape against its own setupTestStack rather than layering it onto a
|
|
5
|
+
// shared one. Proves the catalog's role-gate isn't hardcoded to
|
|
6
|
+
// TenantAdmin/SystemAdmin: an app that restricts billing to a custom "Admin"
|
|
7
|
+
// role locks TenantAdmin out of both the query and the purchase-handlers.
|
|
8
|
+
|
|
9
|
+
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
10
|
+
import type { TenantDb } from "@cosmicdrift/kumiko-framework/db";
|
|
11
|
+
import { defineFeature } from "@cosmicdrift/kumiko-framework/engine";
|
|
12
|
+
import {
|
|
13
|
+
createTestUser,
|
|
14
|
+
setupTestStack,
|
|
15
|
+
type TestStack,
|
|
16
|
+
testTenantId,
|
|
17
|
+
unsafeCreateEntityTable,
|
|
18
|
+
} from "@cosmicdrift/kumiko-framework/stack";
|
|
19
|
+
import {
|
|
20
|
+
createComplianceProfilesFeature,
|
|
21
|
+
tenantComplianceProfileEntity,
|
|
22
|
+
} from "../../compliance-profiles";
|
|
23
|
+
import { createConfigFeature } from "../../config";
|
|
24
|
+
import { createTenantFeature } from "../../tenant/feature";
|
|
25
|
+
import { tenantEntity } from "../../tenant/schema/tenant";
|
|
26
|
+
import { createTenantLifecycleFeature } from "../../tenant-lifecycle";
|
|
27
|
+
import { subscriptionAggregateId } from "../aggregate-id";
|
|
28
|
+
import { SubscriptionFoundationHandlers, SubscriptionStatuses } from "../constants";
|
|
29
|
+
import { createBillingFoundationFeature } from "../feature";
|
|
30
|
+
import { subscriptionsProjectionTable } from "../projection";
|
|
31
|
+
import type { BillingPlanCatalog, ProviderPrice, SubscriptionProviderPlugin } from "../types";
|
|
32
|
+
|
|
33
|
+
const PRICE_TO_TIER: Readonly<Record<string, string>> = { price_pro: "pro" };
|
|
34
|
+
|
|
35
|
+
const PRICES: readonly ProviderPrice[] = [
|
|
36
|
+
{
|
|
37
|
+
priceId: "price_pro",
|
|
38
|
+
unitAmount: 1900,
|
|
39
|
+
currency: "usd",
|
|
40
|
+
interval: "month",
|
|
41
|
+
intervalCount: 1,
|
|
42
|
+
active: true,
|
|
43
|
+
metadata: {},
|
|
44
|
+
},
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
const checkoutCalls: Array<{ priceId: string }> = [];
|
|
48
|
+
|
|
49
|
+
const adminOnlyProviderFeature = defineFeature("test-mock-admin-only-provider", (r) => {
|
|
50
|
+
r.requires("billing-foundation");
|
|
51
|
+
const plugin: SubscriptionProviderPlugin = {
|
|
52
|
+
verifyAndParseWebhook: async () => null,
|
|
53
|
+
priceToTier: PRICE_TO_TIER,
|
|
54
|
+
retrievePrices: async (_ctx, priceIds) => PRICES.filter((p) => priceIds.includes(p.priceId)),
|
|
55
|
+
createCheckoutSession: async (_ctx, options) => {
|
|
56
|
+
checkoutCalls.push({ priceId: options.priceId });
|
|
57
|
+
return { url: `https://mock.example/checkout/${options.priceId}` };
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
r.useExtension("subscriptionProvider", "mock-admin-only-provider", plugin);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
async function resolveCurrentTier(db: TenantDb, tenantId: string): Promise<string> {
|
|
64
|
+
const rows = await db.selectMany(
|
|
65
|
+
subscriptionsProjectionTable,
|
|
66
|
+
{ id: subscriptionAggregateId(tenantId) },
|
|
67
|
+
{ limit: 1 },
|
|
68
|
+
);
|
|
69
|
+
const row = rows[0];
|
|
70
|
+
if (!row) return "free";
|
|
71
|
+
const status = row["status"] as string;
|
|
72
|
+
return status === SubscriptionStatuses.active || status === SubscriptionStatuses.trialing
|
|
73
|
+
? (row["tier"] as string)
|
|
74
|
+
: "free";
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const adminOnlyCatalog: BillingPlanCatalog<"pro"> = {
|
|
78
|
+
plans: ["pro"],
|
|
79
|
+
tierLabelKey: (tier) => `plan.${tier}.label`,
|
|
80
|
+
benefits: () => [],
|
|
81
|
+
resolveCurrentTier,
|
|
82
|
+
viewRoles: ["Admin"],
|
|
83
|
+
purchaseRoles: ["Admin"],
|
|
84
|
+
successPath: "/billing/success",
|
|
85
|
+
cancelPath: "/billing/cancel",
|
|
86
|
+
providerName: "mock-admin-only-provider",
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
let stack: TestStack;
|
|
90
|
+
|
|
91
|
+
beforeAll(async () => {
|
|
92
|
+
stack = await setupTestStack({
|
|
93
|
+
features: [
|
|
94
|
+
createConfigFeature(),
|
|
95
|
+
createTenantFeature(),
|
|
96
|
+
createComplianceProfilesFeature(),
|
|
97
|
+
createTenantLifecycleFeature(),
|
|
98
|
+
createBillingFoundationFeature({
|
|
99
|
+
baseUrl: "https://app.example.com",
|
|
100
|
+
catalog: adminOnlyCatalog,
|
|
101
|
+
}),
|
|
102
|
+
adminOnlyProviderFeature,
|
|
103
|
+
],
|
|
104
|
+
});
|
|
105
|
+
await unsafeCreateEntityTable(stack.db, tenantEntity);
|
|
106
|
+
await unsafeCreateEntityTable(stack.db, tenantComplianceProfileEntity);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
afterAll(async () => {
|
|
110
|
+
await stack.cleanup();
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
describe("billing-plans catalog with viewRoles/purchaseRoles restricted to a custom Admin role", () => {
|
|
114
|
+
test("an Admin can query the catalog and start a checkout through to the mock provider", async () => {
|
|
115
|
+
const admin = createTestUser({ id: 8001, tenantId: testTenantId(8001), roles: ["Admin"] });
|
|
116
|
+
|
|
117
|
+
const result = (await stack.http.queryOk(
|
|
118
|
+
"billing-foundation:query:billing-plans",
|
|
119
|
+
{},
|
|
120
|
+
admin,
|
|
121
|
+
)) as {
|
|
122
|
+
canPurchase: boolean;
|
|
123
|
+
plans: Array<{ tier: string; action: string }>;
|
|
124
|
+
};
|
|
125
|
+
expect(result.canPurchase).toBe(true);
|
|
126
|
+
expect(result.plans.find((p) => p.tier === "pro")?.action).toBe("checkout");
|
|
127
|
+
|
|
128
|
+
const checkout = (await stack.http.writeOk(
|
|
129
|
+
SubscriptionFoundationHandlers.startPlanCheckout,
|
|
130
|
+
{ tier: "pro" },
|
|
131
|
+
admin,
|
|
132
|
+
)) as { url: string };
|
|
133
|
+
expect(checkout.url).toBe("https://mock.example/checkout/price_pro");
|
|
134
|
+
expect(checkoutCalls).toEqual([{ priceId: "price_pro" }]);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test("a TenantAdmin without the Admin role is rejected on both query and start-plan-checkout", async () => {
|
|
138
|
+
const tenantAdmin = createTestUser({
|
|
139
|
+
id: 8002,
|
|
140
|
+
tenantId: testTenantId(8002),
|
|
141
|
+
roles: ["TenantAdmin"],
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
const queryError = await stack.http.queryErr(
|
|
145
|
+
"billing-foundation:query:billing-plans",
|
|
146
|
+
{},
|
|
147
|
+
tenantAdmin,
|
|
148
|
+
);
|
|
149
|
+
expect(queryError.httpStatus).toBe(403);
|
|
150
|
+
|
|
151
|
+
const writeError = await stack.http.writeErr(
|
|
152
|
+
SubscriptionFoundationHandlers.startPlanCheckout,
|
|
153
|
+
{ tier: "pro" },
|
|
154
|
+
tenantAdmin,
|
|
155
|
+
);
|
|
156
|
+
expect(writeError.httpStatus).toBe(403);
|
|
157
|
+
});
|
|
158
|
+
});
|