1import * as Provider from "@/Provider";
2import * as Stripe from "@/Stripe";
3import * as Test from "@/Test/Alchemy";
4import {
5 GetPaymentLink,
6 CreatePrice,
7 CreateProduct,
8 UpdateProduct,
9} from "@distilled.cloud/stripe/stripe";
10import { expect } from "alchemy-test";
11import * as Effect from "effect/Effect";
12import { MinimumLogLevel } from "effect/References";
13import * as Schedule from "effect/Schedule";
14import { isMissingStripeResource } from "@/Stripe/missing.ts";
15
16const { test } = Test.make({ providers: Stripe.providers() });
17
18const logLevel = Effect.provideService(
19 MinimumLogLevel,
20 process.env.DEBUG ? "Debug" : "Info",
21);
22
23const isMissing = isMissingStripeResource;
24
25const waitUntilDeactivated = (id: string) =>
26 GetPaymentLink({ payment_link: id }).pipe(
27 Effect.map((link) =>
28 link.active ? ("active" as const) : ("inactive" as const),
29 ),
30 Effect.catchIf(isMissing, () => Effect.succeed("inactive" as const)),
31 Effect.repeat({
32 schedule: Schedule.spaced("1 second"),
33 until: (status) => status === "inactive",
34 times: 10,
35 }),
36 );
37
38const archiveProduct = (id: string) =>
39 UpdateProduct({ id, active: false }).pipe(
40 Effect.catchIf(isMissing, () => Effect.void),
41 );
42
43test.provider(
44 "create, update, and deactivate a payment link",
45 (stack) =>
46 Effect.gen(function* () {
47 yield* stack.destroy();
48
49 const product = yield* CreateProduct({
50 name: "Alchemy Payment Link Product",
51 });
52 const price = yield* CreatePrice({
53 product: product.id,
54 currency: "usd",
55 unit_amount: 2000,
56 });
57
58 const created = yield* stack.deploy(
59 Effect.gen(function* () {
60 return yield* Stripe.PaymentLink("BuyLink", {
61 lineItems: [{ price: price.id, quantity: 1 }],
62 metadata: { campaign: "launch" },
63 });
64 }),
65 );
66
67 expect(created.id).toMatch(/^plink_/);
68 expect(created.url).toEqual(expect.any(String));
69 expect(created.active).toEqual(true);
70 expect(created.lineItems).toEqual([
71 expect.objectContaining({ price: price.id, quantity: 1 }),
72 ]);
73 expect(created.allowPromotionCodes).toEqual(false);
74 expect(created.billingAddressCollection).toEqual("auto");
75 expect(created.metadata).toMatchObject({ campaign: "launch" });
76 expect(created.livemode).toEqual(false);
77
78 const fetched = yield* GetPaymentLink({
79 payment_link: created.id,
80 });
81 expect(fetched.id).toEqual(created.id);
82 expect(fetched.active).toEqual(true);
83 expect(fetched.metadata?.campaign).toEqual("launch");
84 expect(
85 fetched.metadata?.[Stripe.alchemyMetadataKeys.stack],
86 ).toBeDefined();
87 expect(
88 fetched.metadata?.[Stripe.alchemyMetadataKeys.stage],
89 ).toBeDefined();
90 expect(fetched.metadata?.[Stripe.alchemyMetadataKeys.id]).toBeDefined();
91
92 const updated = yield* stack.deploy(
93 Effect.gen(function* () {
94 return yield* Stripe.PaymentLink("BuyLink", {
95 lineItems: [{ price: price.id, quantity: 2 }],
96 allowPromotionCodes: true,
97 inactiveMessage: "This link is no longer available.",
98 metadata: { campaign: "spring", sku: "pro" },
99 });
100 }),
101 );
102
103 expect(updated.id).toEqual(created.id);
104 expect(updated.lineItems).toEqual([
105 expect.objectContaining({ price: price.id, quantity: 2 }),
106 ]);
107 expect(updated.allowPromotionCodes).toEqual(true);
108 expect(updated.inactiveMessage).toEqual(
109 "This link is no longer available.",
110 );
111 expect(updated.metadata).toEqual({ campaign: "spring", sku: "pro" });
112 expect(updated.active).toEqual(true);
113
114 const refetched = yield* GetPaymentLink({
115 payment_link: updated.id,
116 });
117 expect(refetched.allow_promotion_codes).toEqual(true);
118 expect(refetched.inactive_message).toEqual(
119 "This link is no longer available.",
120 );
121 expect(refetched.metadata?.campaign).toEqual("spring");
122 expect(refetched.metadata?.sku).toEqual("pro");
123 expect(refetched.metadata?.[Stripe.alchemyMetadataKeys.id]).toBeDefined();
124
125 yield* stack.destroy();
126
127 const deactivated = yield* waitUntilDeactivated(created.id);
128 expect(deactivated).toEqual("inactive");
129
130 yield* archiveProduct(product.id);
131 }).pipe(logLevel),
132 {
133 tags: ["provider:stripe", "provider:stripe:paymentlink", "live"],
134 timeout: 120_000,
135 },
136);
137
138test.provider(
139 "list enumerates the deployed payment link",
140 (stack) =>
141 Effect.gen(function* () {
142 yield* stack.destroy();
143
144 const product = yield* CreateProduct({
145 name: "Alchemy Payment Link List Product",
146 });
147 const price = yield* CreatePrice({
148 product: product.id,
149 currency: "usd",
150 unit_amount: 1500,
151 });
152
153 const deployed = yield* stack.deploy(
154 Effect.gen(function* () {
155 return yield* Stripe.PaymentLink("ListLink", {
156 lineItems: [{ price: price.id, quantity: 1 }],
157 metadata: { kind: "list" },
158 });
159 }),
160 );
161
162 const provider = yield* Provider.findProvider(Stripe.PaymentLink);
163 const all = yield* provider.list();
164 const found = all.find((link) => link.id === deployed.id);
165 expect(found).toBeDefined();
166 expect(found?.url).toEqual(deployed.url);
167 expect(found?.metadata).toMatchObject({ kind: "list" });
168 expect(found?.active).toEqual(true);
169
170 yield* stack.destroy();
171
172 const deactivated = yield* waitUntilDeactivated(deployed.id);
173 expect(deactivated).toEqual("inactive");
174
175 const after = yield* provider.list();
176 expect(after.find((link) => link.id === deployed.id)).toBeUndefined();
177
178 yield* archiveProduct(product.id);
179 }).pipe(logLevel),
180 {
181 tags: ["provider:stripe", "provider:stripe:paymentlink", "live"],
182 timeout: 120_000,
183 },
184);
185
186test.provider(
187 "replace when line item price changes",
188 (stack) =>
189 Effect.gen(function* () {
190 yield* stack.destroy();
191
192 const product = yield* CreateProduct({
193 name: "Alchemy Payment Link Replace Product",
194 });
195 const priceV1 = yield* CreatePrice({
196 product: product.id,
197 currency: "usd",
198 unit_amount: 1000,
199 });
200 const priceV2 = yield* CreatePrice({
201 product: product.id,
202 currency: "usd",
203 unit_amount: 2500,
204 });
205
206 const created = yield* stack.deploy(
207 Effect.gen(function* () {
208 return yield* Stripe.PaymentLink("ReplaceLink", {
209 lineItems: [{ price: priceV1.id, quantity: 1 }],
210 });
211 }),
212 );
213
214 const replaced = yield* stack.deploy(
215 Effect.gen(function* () {
216 return yield* Stripe.PaymentLink("ReplaceLink", {
217 lineItems: [{ price: priceV2.id, quantity: 1 }],
218 });
219 }),
220 );
221
222 expect(replaced.id).not.toEqual(created.id);
223 expect(replaced.lineItems).toEqual([
224 expect.objectContaining({ price: priceV2.id, quantity: 1 }),
225 ]);
226
227 const oldFetched = yield* GetPaymentLink({
228 payment_link: created.id,
229 });
230 expect(oldFetched.active).toEqual(false);
231
232 const newFetched = yield* GetPaymentLink({
233 payment_link: replaced.id,
234 });
235 expect(newFetched.active).toEqual(true);
236
237 yield* stack.destroy();
238
239 const deactivated = yield* waitUntilDeactivated(replaced.id);
240 expect(deactivated).toEqual("inactive");
241
242 yield* archiveProduct(product.id);
243 }).pipe(logLevel),
244 {
245 tags: ["provider:stripe", "provider:stripe:paymentlink", "live"],
246 timeout: 120_000,
247 },
248);