1import * as Provider from "@/Provider";
2import * as Stripe from "@/Stripe";
3import * as Test from "@/Test/Alchemy";
4import { isMissingStripeResource } from "@/Stripe/missing.ts";
5import {
6 GetIssuingCards,
7 GetIssuingCard,
8} from "@distilled.cloud/stripe/stripe";
9import { expect } from "alchemy-test";
10import * as Effect from "effect/Effect";
11import * as Result from "effect/Result";
12import { MinimumLogLevel } from "effect/References";
13import * as Schedule from "effect/Schedule";
14
15const { test } = Test.make({ providers: Stripe.providers() });
16
17const ISSUING_ENABLED = process.env.STRIPE_TEST_ISSUING === "1";
18
19const logLevel = Effect.provideService(
20 MinimumLogLevel,
21 process.env.DEBUG ? "Debug" : "Info",
22);
23
24const billing = {
25 address: {
26 line1: "123 Main Street",
27 city: "San Francisco",
28 state: "CA",
29 postalCode: "94111",
30 country: "US",
31 },
32} as const;
33
34const waitUntilCanceled = (id: string) =>
35 GetIssuingCard({ card: id }).pipe(
36 Effect.map((card) =>
37 card.status === "canceled" ? ("canceled" as const) : ("live" as const),
38 ),
39 Effect.catchIf(isMissingStripeResource, () =>
40 Effect.succeed("canceled" as const),
41 ),
42 Effect.repeat({
43 schedule: Schedule.spaced("1 second"),
44 until: (status) => status === "canceled",
45 times: 10,
46 }),
47 );
48
49test.provider(
50 "issuing cards entitlement probe",
51 (stack) =>
52 Effect.gen(function* () {
53 yield* stack.destroy();
54
55 const result = yield* GetIssuingCards({ limit: 1 }).pipe(Effect.result);
56
57 if (Result.isSuccess(result)) {
58 expect(Array.isArray(result.success.data)).toBe(true);
59 } else {
60 expect(result.failure._tag).not.toEqual("UnknownStripeError");
61 expect(["InvalidRequestError", "Forbidden", "Unauthorized"]).toContain(
62 result.failure._tag,
63 );
64 if (result.failure._tag === "InvalidRequestError") {
65 expect(result.failure.message).toContain("not set up to use Issuing");
66 }
67 }
68
69 yield* stack.destroy();
70 }).pipe(logLevel),
71 {
72 tags: ["provider:stripe", "provider:stripe:issuingcard", "live"],
73 timeout: 60_000,
74 },
75);
76
77test.provider.skipIf(!ISSUING_ENABLED)(
78 "create, update, and cancel an issuing card",
79 (stack) =>
80 Effect.gen(function* () {
81 yield* stack.destroy();
82
83 const created = yield* stack.deploy(
84 Effect.gen(function* () {
85 const cardholder = yield* Stripe.IssuingCardholder("ExpenseHolder", {
86 name: "Alchemy Card Holder",
87 type: "individual",
88 email: "alchemy.issuing.card@example.com",
89 phoneNumber: "+15555550100",
90 billing,
91 individual: { firstName: "Alchemy", lastName: "Tester" },
92 });
93 const card = yield* Stripe.IssuingCard("ExpenseCard", {
94 cardholder: cardholder.id,
95 currency: "usd",
96 type: "virtual",
97 status: "inactive",
98 metadata: { team: "ops" },
99 });
100 return { card, cardholder };
101 }),
102 );
103
104 expect(created.card.id).toMatch(/^ic_/);
105 expect(created.card.cardholder).toEqual(created.cardholder.id);
106 expect(created.card.currency).toEqual("usd");
107 expect(created.card.type).toEqual("virtual");
108 expect(created.card.status).toEqual("inactive");
109 expect(created.card.brand).toEqual(expect.any(String));
110 expect(created.card.last4).toEqual(expect.any(String));
111 expect(created.card.expMonth).toEqual(expect.any(Number));
112 expect(created.card.expYear).toEqual(expect.any(Number));
113 expect(created.card.metadata).toMatchObject({ team: "ops" });
114 expect(created.card.livemode).toEqual(false);
115
116 const fetched = yield* GetIssuingCard({ card: created.card.id });
117 expect(fetched.id).toEqual(created.card.id);
118 expect(fetched.cardholder.id).toEqual(created.cardholder.id);
119 expect(fetched.currency).toEqual("usd");
120 expect(fetched.type).toEqual("virtual");
121 expect(fetched.status).toEqual("inactive");
122 expect(fetched.metadata?.team).toEqual("ops");
123 expect(
124 fetched.metadata?.[Stripe.alchemyMetadataKeys.stack],
125 ).toBeDefined();
126 expect(
127 fetched.metadata?.[Stripe.alchemyMetadataKeys.stage],
128 ).toBeDefined();
129 expect(fetched.metadata?.[Stripe.alchemyMetadataKeys.id]).toBeDefined();
130
131 const updated = yield* stack.deploy(
132 Effect.gen(function* () {
133 const cardholder = yield* Stripe.IssuingCardholder("ExpenseHolder", {
134 name: "Alchemy Card Holder",
135 type: "individual",
136 email: "alchemy.issuing.card@example.com",
137 phoneNumber: "+15555550100",
138 billing,
139 individual: { firstName: "Alchemy", lastName: "Tester" },
140 });
141 const card = yield* Stripe.IssuingCard("ExpenseCard", {
142 cardholder: cardholder.id,
143 currency: "usd",
144 type: "virtual",
145 status: "inactive",
146 spendingControls: {
147 spendingLimits: [{ amount: 50_000, interval: "monthly" }],
148 },
149 metadata: { team: "finance", sku: "ic-2" },
150 });
151 return { card, cardholder };
152 }),
153 );
154
155 expect(updated.card.id).toEqual(created.card.id);
156 expect(updated.card.status).toEqual("inactive");
157 expect(updated.card.metadata).toEqual({ team: "finance", sku: "ic-2" });
158 expect(updated.card.spendingControls?.spendingLimits).toEqual([
159 { amount: 50_000, interval: "monthly" },
160 ]);
161
162 const refetched = yield* GetIssuingCard({ card: updated.card.id });
163 expect(refetched.id).toEqual(updated.card.id);
164 expect(refetched.status).toEqual("inactive");
165 expect(refetched.metadata?.team).toEqual("finance");
166 expect(refetched.metadata?.sku).toEqual("ic-2");
167 expect(refetched.metadata?.[Stripe.alchemyMetadataKeys.id]).toBeDefined();
168 expect(refetched.spending_controls.spending_limits).toEqual(
169 expect.arrayContaining([
170 expect.objectContaining({
171 amount: 50_000,
172 interval: "monthly",
173 }),
174 ]),
175 );
176
177 yield* stack.destroy();
178
179 const canceled = yield* waitUntilCanceled(created.card.id);
180 expect(canceled).toEqual("canceled");
181 }).pipe(logLevel),
182 {
183 tags: [
184 "provider:stripe",
185 "provider:stripe:issuingcard",
186 "provider:stripe:issuingcardholder",
187 "live",
188 ],
189 timeout: 120_000,
190 },
191);
192
193test.provider.skipIf(!ISSUING_ENABLED)(
194 "list enumerates the deployed issuing card",
195 (stack) =>
196 Effect.gen(function* () {
197 yield* stack.destroy();
198
199 const deployed = yield* stack.deploy(
200 Effect.gen(function* () {
201 const cardholder = yield* Stripe.IssuingCardholder("ListHolder", {
202 name: "Alchemy List Holder",
203 type: "individual",
204 email: "alchemy.issuing.list@example.com",
205 phoneNumber: "+15555550100",
206 billing,
207 individual: { firstName: "Alchemy", lastName: "Lister" },
208 });
209 const card = yield* Stripe.IssuingCard("ListCard", {
210 cardholder: cardholder.id,
211 currency: "usd",
212 type: "virtual",
213 status: "inactive",
214 metadata: { kind: "list" },
215 });
216 return { card, cardholder };
217 }),
218 );
219
220 const provider = yield* Provider.findProvider(Stripe.IssuingCard);
221 const all = yield* provider.list();
222 const found = all.find((card) => card.id === deployed.card.id);
223 expect(found).toBeDefined();
224 expect(found?.cardholder).toEqual(deployed.cardholder.id);
225 expect(found?.type).toEqual("virtual");
226 expect(found?.metadata).toMatchObject({ kind: "list" });
227
228 yield* stack.destroy();
229
230 const canceled = yield* waitUntilCanceled(deployed.card.id);
231 expect(canceled).toEqual("canceled");
232
233 const after = yield* provider.list();
234 expect(
235 after.find((card) => card.id === deployed.card.id),
236 ).toBeUndefined();
237 }).pipe(logLevel),
238 {
239 tags: [
240 "provider:stripe",
241 "provider:stripe:issuingcard",
242 "provider:stripe:issuingcardholder",
243 "live",
244 ],
245 timeout: 120_000,
246 },
247);