1import * as Provider from "@/Provider";
2import * as Stripe from "@/Stripe";
3import * as Test from "@/Test/Alchemy";
4import { GetBillingCreditGrant } from "@distilled.cloud/stripe/stripe";
5import { expect } from "alchemy-test";
6import * as Effect from "effect/Effect";
7import { MinimumLogLevel } from "effect/References";
8import * as Schedule from "effect/Schedule";
9import { isMissingStripeResource } from "@/Stripe/missing.ts";
10
11const { test } = Test.make({ providers: Stripe.providers() });
12
13const logLevel = Effect.provideService(
14 MinimumLogLevel,
15 process.env.DEBUG ? "Debug" : "Info",
16);
17
18const EXPIRES_AT = 4_102_444_800;
19
20const waitUntilVoided = (id: string) =>
21 GetBillingCreditGrant({ id }).pipe(
22 Effect.map((grant) =>
23 grant.voided_at != null ? ("voided" as const) : ("active" as const),
24 ),
25 Effect.catchIf(isMissingStripeResource, () =>
26 Effect.succeed("gone" as const),
27 ),
28 Effect.repeat({
29 schedule: Schedule.spaced("1 second"),
30 until: (status) => status === "voided" || status === "gone",
31 times: 10,
32 }),
33 );
34
35test.provider(
36 "create, update, and void a credit grant",
37 (stack) =>
38 Effect.gen(function* () {
39 yield* stack.destroy();
40
41 const created = yield* stack.deploy(
42 Effect.gen(function* () {
43 const customer = yield* Stripe.Customer("GrantCustomer", {
44 email: "alchemy.credit.grant@example.com",
45 name: "Alchemy Credit Grant Customer",
46 });
47 return yield* Stripe.CreditGrant("WelcomeCredits", {
48 customer: customer.id,
49 amount: {
50 type: "monetary",
51 monetary: { currency: "usd", value: 1000 },
52 },
53 applicabilityConfig: { scope: { priceType: "metered" } },
54 category: "promotional",
55 name: "Alchemy Welcome Credits",
56 metadata: { campaign: "welcome" },
57 });
58 }),
59 );
60
61 expect(created.id).toMatch(/^credgr_/);
62 expect(created.customer).toMatch(/^cus_/);
63 expect(created.amount).toEqual({
64 type: "monetary",
65 monetary: { currency: "usd", value: 1000 },
66 });
67 expect(created.applicabilityConfig).toEqual({
68 scope: { priceType: "metered" },
69 });
70 expect(created.category).toEqual("promotional");
71 expect(created.name).toEqual("Alchemy Welcome Credits");
72 expect(created.voidedAt).toBeUndefined();
73 expect(created.livemode).toEqual(false);
74 expect(created.metadata).toMatchObject({ campaign: "welcome" });
75 expect(created.created).toEqual(expect.any(Number));
76
77 const fetched = yield* GetBillingCreditGrant({ id: created.id });
78 expect(fetched.id).toEqual(created.id);
79 expect(fetched.amount.monetary?.currency).toEqual("usd");
80 expect(fetched.amount.monetary?.value).toEqual(1000);
81 expect(fetched.applicability_config.scope.price_type).toEqual("metered");
82 expect(fetched.category).toEqual("promotional");
83 expect(fetched.name).toEqual("Alchemy Welcome Credits");
84 expect(fetched.voided_at).toBeNull();
85 expect(fetched.metadata?.campaign).toEqual("welcome");
86 expect(
87 fetched.metadata?.[Stripe.alchemyMetadataKeys.stack],
88 ).toBeDefined();
89 expect(
90 fetched.metadata?.[Stripe.alchemyMetadataKeys.stage],
91 ).toBeDefined();
92 expect(fetched.metadata?.[Stripe.alchemyMetadataKeys.id]).toBeDefined();
93
94 const updated = yield* stack.deploy(
95 Effect.gen(function* () {
96 const customer = yield* Stripe.Customer("GrantCustomer", {
97 email: "alchemy.credit.grant@example.com",
98 name: "Alchemy Credit Grant Customer",
99 });
100 return yield* Stripe.CreditGrant("WelcomeCredits", {
101 customer: customer.id,
102 amount: {
103 type: "monetary",
104 monetary: { currency: "usd", value: 1000 },
105 },
106 applicabilityConfig: { scope: { priceType: "metered" } },
107 category: "promotional",
108 name: "Alchemy Welcome Credits",
109 expiresAt: EXPIRES_AT,
110 metadata: { campaign: "spring", sku: "credits-2" },
111 });
112 }),
113 );
114
115 expect(updated.id).toEqual(created.id);
116 expect(updated.expiresAt).toEqual(EXPIRES_AT);
117 expect(updated.metadata).toEqual({
118 campaign: "spring",
119 sku: "credits-2",
120 });
121 expect(updated.amount.monetary?.value).toEqual(1000);
122
123 const refetched = yield* GetBillingCreditGrant({ id: updated.id });
124 expect(refetched.id).toEqual(updated.id);
125 expect(refetched.expires_at).toEqual(EXPIRES_AT);
126 expect(refetched.metadata?.campaign).toEqual("spring");
127 expect(refetched.metadata?.sku).toEqual("credits-2");
128 expect(refetched.metadata?.[Stripe.alchemyMetadataKeys.id]).toBeDefined();
129 expect(refetched.voided_at).toBeNull();
130
131 yield* stack.destroy();
132
133 const voided = yield* waitUntilVoided(created.id);
134 expect(voided === "voided" || voided === "gone").toEqual(true);
135 if (voided === "voided") {
136 const deactivated = yield* GetBillingCreditGrant({
137 id: created.id,
138 });
139 expect(deactivated.voided_at).toEqual(expect.any(Number));
140 }
141 }).pipe(logLevel),
142 {
143 tags: [
144 "provider:stripe",
145 "provider:stripe:creditgrant",
146 "provider:stripe:customer",
147 "live",
148 ],
149 timeout: 120_000,
150 },
151);
152
153test.provider(
154 "list enumerates the deployed credit grant",
155 (stack) =>
156 Effect.gen(function* () {
157 yield* stack.destroy();
158
159 const deployed = yield* stack.deploy(
160 Effect.gen(function* () {
161 const customer = yield* Stripe.Customer("ListGrantCustomer", {
162 email: "alchemy.credit.grant.list@example.com",
163 name: "Alchemy Credit Grant List Customer",
164 });
165 return yield* Stripe.CreditGrant("ListCredits", {
166 customer: customer.id,
167 amount: {
168 type: "monetary",
169 monetary: { currency: "usd", value: 2500 },
170 },
171 applicabilityConfig: { scope: { priceType: "metered" } },
172 category: "paid",
173 name: "Alchemy List Credits",
174 metadata: { kind: "list" },
175 });
176 }),
177 );
178
179 const provider = yield* Provider.findProvider(Stripe.CreditGrant);
180 const all = yield* provider.list();
181 const found = all.find((grant) => grant.id === deployed.id);
182 expect(found).toBeDefined();
183 expect(found?.name).toEqual(deployed.name);
184 expect(found?.amount.monetary?.value).toEqual(2500);
185 expect(found?.metadata).toMatchObject({ kind: "list" });
186 expect(found?.voidedAt).toBeUndefined();
187
188 yield* stack.destroy();
189
190 const voided = yield* waitUntilVoided(deployed.id);
191 expect(voided === "voided" || voided === "gone").toEqual(true);
192
193 const after = yield* provider.list();
194 expect(after.find((grant) => grant.id === deployed.id)).toBeUndefined();
195 }).pipe(logLevel),
196 {
197 tags: [
198 "provider:stripe",
199 "provider:stripe:creditgrant",
200 "provider:stripe:customer",
201 "live",
202 ],
203 timeout: 120_000,
204 },
205);
206
207test.provider(
208 "replace when the amount changes",
209 (stack) =>
210 Effect.gen(function* () {
211 yield* stack.destroy();
212
213 const created = yield* stack.deploy(
214 Effect.gen(function* () {
215 const customer = yield* Stripe.Customer("ReplaceGrantCustomer", {
216 email: "alchemy.credit.grant.replace@example.com",
217 name: "Alchemy Credit Grant Replace Customer",
218 });
219 return yield* Stripe.CreditGrant("ReplaceCredits", {
220 customer: customer.id,
221 amount: {
222 type: "monetary",
223 monetary: { currency: "usd", value: 1000 },
224 },
225 applicabilityConfig: { scope: { priceType: "metered" } },
226 category: "promotional",
227 name: "Alchemy Replace Credits",
228 });
229 }),
230 );
231
232 expect(created.amount.monetary?.value).toEqual(1000);
233
234 const replaced = yield* stack.deploy(
235 Effect.gen(function* () {
236 const customer = yield* Stripe.Customer("ReplaceGrantCustomer", {
237 email: "alchemy.credit.grant.replace@example.com",
238 name: "Alchemy Credit Grant Replace Customer",
239 });
240 return yield* Stripe.CreditGrant("ReplaceCredits", {
241 customer: customer.id,
242 amount: {
243 type: "monetary",
244 monetary: { currency: "usd", value: 2000 },
245 },
246 applicabilityConfig: { scope: { priceType: "metered" } },
247 category: "promotional",
248 name: "Alchemy Replace Credits",
249 });
250 }),
251 );
252
253 expect(replaced.id).not.toEqual(created.id);
254 expect(replaced.amount.monetary?.value).toEqual(2000);
255
256 const fetched = yield* GetBillingCreditGrant({ id: replaced.id });
257 expect(fetched.id).toEqual(replaced.id);
258 expect(fetched.amount.monetary?.value).toEqual(2000);
259
260 const oldVoided = yield* waitUntilVoided(created.id);
261 expect(oldVoided === "voided" || oldVoided === "gone").toEqual(true);
262
263 yield* stack.destroy();
264
265 const gone = yield* waitUntilVoided(replaced.id);
266 expect(gone === "voided" || gone === "gone").toEqual(true);
267 }).pipe(logLevel),
268 {
269 tags: [
270 "provider:stripe",
271 "provider:stripe:creditgrant",
272 "provider:stripe:customer",
273 "live",
274 ],
275 timeout: 120_000,
276 },
277);