1import * as GCP from "@/GCP";
2import * as Test from "@/Test/Alchemy";
3import * as androidpublisher from "@distilled.cloud/gcp/androidpublisher_v3";
4import { expect } from "alchemy-test";
5import * as Effect from "effect/Effect";
6import * as Schedule from "effect/Schedule";
7import {
8 logLevel,
9 packageName,
10 probePackageName,
11 missingTag,
12 runLifecycle,
13} from "./common.ts";
14
15const { test } = Test.make({ providers: GCP.providers() });
16
17const waitUntilGone = (
18 appId: string,
19 productId: string,
20 basePlanId: string,
21 offerId: string,
22) =>
23 androidpublisher
24 .getMonetizationSubscriptionsBasePlansOffers({
25 packageName: appId,
26 productId,
27 basePlanId,
28 offerId,
29 })
30 .pipe(
31 Effect.as("found" as const),
32 Effect.catchTag("NotFound", () => Effect.succeed("gone" as const)),
33 Effect.repeat({
34 schedule: Schedule.spaced("1 second"),
35 until: (status) => status === "gone",
36 times: 10,
37 }),
38 );
39
40test.provider(
41 "getMonetizationSubscriptionsBasePlansOffers on a missing offer fails with a typed tag",
42 (stack) =>
43 Effect.gen(function* () {
44 yield* stack.destroy();
45
46 const error = yield* Effect.flip(
47 androidpublisher.getMonetizationSubscriptionsBasePlansOffers({
48 packageName: probePackageName,
49 productId: "alchemy_missing_sub",
50 basePlanId: "monthly",
51 offerId: "alchemy-missing-offer",
52 }),
53 );
54 expect(error._tag).toEqual(missingTag);
55
56 yield* stack.destroy();
57 }).pipe(logLevel),
58 {
59 tags: ["provider:gcp", "provider:gcp:androidpublisher", "live"],
60 timeout: 90_000,
61 },
62);
63
64test.provider.skipIf(runLifecycle)(
65 "createMonetizationSubscriptionsBasePlansOffers without Play access fails with a typed tag",
66 (stack) =>
67 Effect.gen(function* () {
68 yield* stack.destroy();
69
70 const error = yield* Effect.flip(
71 androidpublisher.createMonetizationSubscriptionsBasePlansOffers({
72 packageName: probePackageName,
73 productId: "alchemy_probe_sub",
74 basePlanId: "monthly",
75 offerId: "intro",
76 "regionsVersion.version": "2025/01",
77 body: {
78 packageName: probePackageName,
79 productId: "alchemy_probe_sub",
80 basePlanId: "monthly",
81 offerId: "intro",
82 phases: [
83 {
84 duration: "P1W",
85 recurrenceCount: 1,
86 regionalConfigs: [{ regionCode: "US", free: {} }],
87 },
88 ],
89 regionalConfigs: [
90 { regionCode: "US", newSubscriberAvailability: true },
91 ],
92 },
93 }),
94 );
95 expect(error._tag).toEqual("Forbidden");
96
97 yield* stack.destroy();
98 }).pipe(logLevel),
99 {
100 tags: ["provider:gcp", "provider:gcp:androidpublisher", "live"],
101 timeout: 90_000,
102 },
103);
104
105test.provider.skipIf(!runLifecycle)(
106 "create, update, and delete a subscription offer",
107 (stack) =>
108 Effect.gen(function* () {
109 yield* stack.destroy();
110
111 const created = yield* stack.deploy(
112 Effect.gen(function* () {
113 const subscription =
114 yield* GCP.AndroidPublisher.MonetizationSubscription("Premium", {
115 packageName: packageName!,
116 listings: [{ languageCode: "en-US", title: "Premium" }],
117 basePlans: [
118 {
119 basePlanId: "monthly",
120 autoRenewingBasePlanType: { billingPeriodDuration: "P1M" },
121 regionalConfigs: [
122 {
123 regionCode: "US",
124 newSubscriberAvailability: true,
125 price: { currencyCode: "USD", units: "5" },
126 },
127 ],
128 },
129 ],
130 });
131 const offer =
132 yield* GCP.AndroidPublisher.MonetizationSubscriptionsBasePlansOffer(
133 "Intro",
134 {
135 packageName: packageName!,
136 productId: subscription.productId,
137 basePlanId: "monthly",
138 },
139 );
140 return { subscription, offer };
141 }),
142 );
143
144 expect(created.offer.offerId.length).toBeGreaterThan(0);
145 expect(created.offer.productId).toEqual(created.subscription.productId);
146
147 const fetched =
148 yield* androidpublisher.getMonetizationSubscriptionsBasePlansOffers({
149 packageName: created.offer.packageName,
150 productId: created.offer.productId,
151 basePlanId: created.offer.basePlanId,
152 offerId: created.offer.offerId,
153 });
154 expect(fetched.offerId).toEqual(created.offer.offerId);
155
156 const updated = yield* stack.deploy(
157 Effect.gen(function* () {
158 const subscription =
159 yield* GCP.AndroidPublisher.MonetizationSubscription("Premium", {
160 packageName: created.subscription.packageName,
161 productId: created.subscription.productId,
162 listings: [{ languageCode: "en-US", title: "Premium" }],
163 basePlans: [
164 {
165 basePlanId: "monthly",
166 autoRenewingBasePlanType: { billingPeriodDuration: "P1M" },
167 regionalConfigs: [
168 {
169 regionCode: "US",
170 newSubscriberAvailability: true,
171 price: { currencyCode: "USD", units: "5" },
172 },
173 ],
174 },
175 ],
176 });
177 const offer =
178 yield* GCP.AndroidPublisher.MonetizationSubscriptionsBasePlansOffer(
179 "Intro",
180 {
181 packageName: created.offer.packageName,
182 productId: created.offer.productId,
183 basePlanId: created.offer.basePlanId,
184 offerId: created.offer.offerId,
185 regionalConfigs: [
186 { regionCode: "US", newSubscriberAvailability: false },
187 ],
188 },
189 );
190 return { subscription, offer };
191 }),
192 );
193
194 expect(updated.offer.offerId).toEqual(created.offer.offerId);
195 expect(
196 updated.offer.regionalConfigs?.[0]?.newSubscriberAvailability,
197 ).toEqual(false);
198
199 yield* stack.destroy();
200
201 const gone = yield* waitUntilGone(
202 created.offer.packageName,
203 created.offer.productId,
204 created.offer.basePlanId,
205 created.offer.offerId,
206 );
207 expect(gone).toEqual("gone");
208 }).pipe(logLevel),
209 {
210 tags: ["provider:gcp", "provider:gcp:androidpublisher", "live"],
211 timeout: 90_000,
212 },
213);