← Provider replacement walkthrough

IAPKit · convex/subscriptions/internal.test.ts

Source snapshot · 2026-10-21

SHA-256 4e907c3fe8fff179b076bf1f8d9a87ddf2deff794b5cac695fe1169f569cc960
1import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";2 3import { compareValues } from "convex/values";4import type { Doc, Id } from "../_generated/dataModel";5import { accessAt } from "./accessHistory";6import { addSubscriptionToCohorts, type CohortBucket } from "./revenueCohorts";7import { normalizeAppleAsn } from "../webhooks/shared";8import { HarmonizedPurchaseState } from "../purchases/purchaseState";9import { hmacSha256Hex } from "../utils/sha256";10import {11  applySubscriptionEventHandler,12  bindSubscriptionToUserHandler,13  rebindSubscriptionToUserHandler,14  drainSubscriptionUserErasurePage,15  buildVerifiedSubscriptionSnapshot,16  getCurrentProductIdByTokenHandler,17  getSourceProductIdByTokenHandler,18  mergeVerifiedSubscriptionSnapshot,19  recordVerifiedSubscriptionHandler,20  persistSubscriptionSnapshot,21} from "./internal";22 23type Row = Record<string, unknown> & { _id: string; _creationTime: number };24 25class MemQuery {26  constructor(private rows: Row[]) {}27 28  withIndex(_name: string, cb?: (q: IndexBuilder) => IndexBuilder): MemQuery {29    if (!cb) return this;30    const builder = new IndexBuilder();31    cb(builder);32    return new MemQuery(33      this.rows.filter((row) =>34        builder.predicates.every((predicate) => predicate(row)),35      ),36    );37  }38 39  async unique(): Promise<Row | null> {40    if (this.rows.length > 1) {41      throw new Error("unique() called on a query that returned > 1 row");42    }43    return this.rows[0] ?? null;44  }45 46  async collect(): Promise<Row[]> {47    return [...this.rows];48  }49 50  async paginate(options: { numItems: number; cursor: string | null }) {51    const key = (row: Row) => [row._creationTime, row._id];52    const boundary =53      options.cursor === null ? null : JSON.parse(options.cursor);54    const ordered = [...this.rows].sort((a, b) =>55      compareValues(key(a), key(b)),56    );57    const remaining = ordered.filter(58      (row) => boundary === null || compareValues(key(row), boundary) > 0,59    );60    const page = remaining.slice(0, options.numItems);61    return {62      page: structuredClone(page),63      isDone: page.length === remaining.length,64      continueCursor: page.length65        ? JSON.stringify(key(page[page.length - 1]))66        : (options.cursor ?? ""),67    };68  }69 70  async take(count: number): Promise<Row[]> {71    return this.rows.slice(0, count);72  }73}74 75class IndexBuilder {76  predicates: Array<(row: Row) => boolean> = [];77 78  eq(field: string, value: unknown): IndexBuilder {79    this.predicates.push((row) => row[field] === value);80    return this;81  }82}83 84class MemDb {85  tables = new Map<string, Map<string, Row>>();86  private counter = 0;87 88  constructor() {89    this.table("organizations").set("organizations_seed_1", {90      _id: "organizations_seed_1",91      _creationTime: Date.now(),92    });93    this.table("projects").set("projects_seed_1", {94      _id: "projects_seed_1",95      _creationTime: Date.now(),96      organizationId: "organizations_seed_1",97    });98  }99 100  private table(name: string): Map<string, Row> {101    let table = this.tables.get(name);102    if (!table) {103      table = new Map();104      this.tables.set(name, table);105    }106    return table;107  }108 109  query(tableName: string): MemQuery {110    return new MemQuery([...this.table(tableName).values()]);111  }112 113  async insert(114    tableName: string,115    doc: Record<string, unknown>,116  ): Promise<string> {117    const id = `${tableName}_${++this.counter}`;118    this.table(tableName).set(id, {119      ...doc,120      _id: id,121      _creationTime: Date.now() + this.counter / 1_000,122    });123    return id;124  }125 126  async get(id: string): Promise<Row | null> {127    for (const table of this.tables.values()) {128      const row = table.get(id);129      if (row) return row;130    }131    return null;132  }133 134  async patch(id: string, patch: Record<string, unknown>): Promise<void> {135    for (const table of this.tables.values()) {136      const row = table.get(id);137      if (row) {138        Object.assign(row, patch);139        return;140      }141    }142    throw new Error(`patch: no doc with id ${id}`);143  }144 145  async delete(id: string): Promise<void> {146    for (const table of this.tables.values()) {147      if (table.delete(id)) return;148    }149  }150 151  rows(tableName: string): Row[] {152    return [...this.table(tableName).values()];153  }154 155  seedProduct(doc: {156    projectId: string;157    platform: "IOS" | "Android";158    productId: string;159    billingPeriod?: string;160  }): void {161    this.table("products").set(`products_${++this.counter}`, {162      _id: `products_${this.counter}`,163      _creationTime: Date.now(),164      ...doc,165    });166  }167}168 169function makeCtx(db: MemDb) {170  return { db, scheduler: { runAfter: async () => null } } as never;171}172 173const PROJECT_ID = "projects_seed_1";174const TOKEN = "purchase_token_1";175 176describe("entitlement events across the expiry deadline", () => {177  it.each([false, true])(178    "revokes once when the clock expires before notifications (cancel first: %s)",179    async (cancelFirst) => {180      vi.useFakeTimers();181      try {182        vi.setSystemTime(1_000);183        const db = new MemDb();184        const started = await seedWebhookEvent(db, {185          type: "SubscriptionStarted",186          notificationId: "clock-start",187          occurredAt: 1_000,188        });189        await db.patch(started, { expiresAt: 2_000, renewsAt: 2_000 });190        await applySubscriptionEventHandler(makeCtx(db), {191          projectId: PROJECT_ID as never,192          eventId: started as never,193        });194        await bindSubscriptionToUserHandler(makeCtx(db), {195          projectId: PROJECT_ID as never,196          purchaseToken: TOKEN,197          userId: "clock-user",198        });199        vi.setSystemTime(3_000);200        if (cancelFirst) {201          const canceled = await seedWebhookEvent(db, {202            type: "SubscriptionStarted",203            notificationId: "clock-cancel",204            occurredAt: 2_500,205          });206          await db.patch(canceled, {207            type: "SubscriptionCanceled",208            expiresAt: 2_000,209            renewsAt: undefined,210            willRenew: false,211          });212          await applySubscriptionEventHandler(makeCtx(db), {213            projectId: PROJECT_ID as never,214            eventId: canceled as never,215          });216        }217        const expired = await seedWebhookEvent(db, {218          type: "SubscriptionExpired",219          notificationId: "clock-expire",220          occurredAt: 3_000,221        });222        await db.patch(expired, { expiresAt: 2_000 });223        const args = {224          projectId: PROJECT_ID as never,225          eventId: expired as never,226        };227        await applySubscriptionEventHandler(makeCtx(db), args);228        await applySubscriptionEventHandler(makeCtx(db), args);229        expect(230          db231            .rows("commerceEvents")232            .filter((row) => String(row.eventType).startsWith("entitlement."))233            .map((row) => row.eventType),234        ).toEqual(["entitlement.granted", "entitlement.revoked"]);235      } finally {236        vi.useRealTimers();237      }238    },239  );240});241 242async function seedWebhookEvent(243  db: MemDb,244  args: {245    type:246      | "SubscriptionStarted"247      | "SubscriptionRenewed"248      | "SubscriptionExpired"249      | "SubscriptionProductChanged";250    notificationId: string;251    occurredAt: number;252    platform?: "IOS" | "Android";253    productId?: string;254    environment?: "Production" | "Sandbox";255  },256): Promise<string> {257  const platform = args.platform ?? "Android";258  return await db.insert("webhookEvents", {259    projectId: PROJECT_ID,260    type: args.type,261    source:262      platform === "IOS"263        ? "AppleAppStoreServerNotificationsV2"264        : "GooglePlayRealTimeDeveloperNotifications",265    platform,266    environment: args.environment ?? "Production",267    purchaseToken: TOKEN,268    sourceNotificationId: args.notificationId,269    productId: args.productId ?? "premium_monthly",270    subscriptionState:271      args.type === "SubscriptionExpired" ? "Expired" : "Active",272    expiresAt: 1_800_000_000_000,273    renewsAt: 1_800_000_000_000,274    currency: "USD",275    priceAmountMicros: 9_990_000,276    occurredAt: args.occurredAt,277    receivedAt: args.occurredAt,278  });279}280 281describe("subscription erasure across replacement tokens", () => {282  it.each([false, true])(283    "reconciles a granted successor without restoring erased identities (successor erasure requested: %s)",284    async (successorErased) => {285      const db = new MemDb();286      const ctx = makeCtx(db);287      const hashKey = "erasure-test-key";288      await db.patch(PROJECT_ID, { userErasureHashKey: hashKey });289      async function erasureJob(userId: string): Promise<string> {290        return db.insert("subscriptionUserErasureJobs", {291          projectId: PROJECT_ID,292          userId,293          userIdHash: await hmacSha256Hex(hashKey, userId),294          status: "queued",295          subscriptionsErased: 0,296          commerceEventsErased: 0,297        });298      }299      await recordVerifiedSubscriptionHandler(ctx, {300        projectId: PROJECT_ID as never,301        platform: "Android",302        purchaseToken: TOKEN,303        productId: "premium_monthly",304        purchaseState: "ENTITLED",305      });306      await bindSubscriptionToUserHandler(ctx, {307        projectId: PROJECT_ID as never,308        purchaseToken: TOKEN,309        userId: "erased-owner",310      });311      await drainSubscriptionUserErasurePage(312        ctx,313        (await erasureJob("erased-owner")) as never,314      );315      const startedId = await seedWebhookEvent(db, {316        type: "SubscriptionStarted",317        notificationId: "successor-granted",318        occurredAt: 1_000,319      });320      await db.patch(startedId, {321        purchaseToken: "replacement",322        expiresAt: Date.now() + 60_000,323      });324      await applySubscriptionEventHandler(ctx, {325        projectId: PROJECT_ID as never,326        eventId: startedId as never,327      });328      await bindSubscriptionToUserHandler(ctx, {329        projectId: PROJECT_ID as never,330        purchaseToken: "replacement",331        userId: "successor-owner",332      });333      const grant = db334        .rows("commerceEvents")335        .find((event) => event.eventType === "entitlement.granted");336      expect(grant?.userId).toBe("successor-owner");337 338      const successorErasureJob = successorErased339        ? await erasureJob("successor-owner")340        : undefined;341      // A destination exists so an emitted revocation would be delivered;342      // none is, which is the point of the assertions below.343      await db.insert("outboundDestinations", {344        projectId: PROJECT_ID,345        enabled: true,346        eventTypes: ["entitlement.revoked"],347      });348      const linkedId = await seedWebhookEvent(db, {349        type: "SubscriptionProductChanged",350        notificationId: "late-erased-link",351        occurredAt: 2_000,352      });353      await db.patch(linkedId, {354        purchaseToken: "replacement",355        linkedPurchaseToken: TOKEN,356      });357      const args = {358        projectId: PROJECT_ID as never,359        eventId: linkedId as never,360      };361      await applySubscriptionEventHandler(ctx, args);362      await applySubscriptionEventHandler(ctx, args);363 364      // The erased owner was unlinked, not the subscription. The successor365      // bound the replacement token afterwards, so that live association366      // survives the merge and nothing is revoked.367      expect(db.rows("subscriptions")).toHaveLength(1);368      expect(db.rows("subscriptions")[0].accountErased).toBeUndefined();369      expect(db.rows("subscriptions")[0].userId).toBe("successor-owner");370      expect(371        db372          .rows("commerceEvents")373          .filter((event) => event.eventType === "entitlement.revoked"),374      ).toEqual([]);375      expect(db.rows("outboundDeliveries")).toEqual([]);376      if (successorErased) {377        await drainSubscriptionUserErasurePage(378          ctx,379          successorErasureJob as never,380        );381        expect(382          db.rows("commerceEvents").some((event) => event.userId !== undefined),383        ).toBe(false);384      }385      expect(386        JSON.stringify(387          [...db.tables.values()].map((rows) => [...rows.values()]),388        ),389      ).not.toContain("erased-owner");390    },391  );392 393  it.each([394    { erasedToken: TOKEN, successorExists: false, boundToken: undefined },395    { erasedToken: TOKEN, successorExists: true, boundToken: undefined },396    { erasedToken: TOKEN, successorExists: true, boundToken: "replacement" },397    { erasedToken: "replacement", successorExists: true, boundToken: TOKEN },398  ])(399    "merges an erased record without dropping a live binding: %j",400    async (testCase) => {401      const db = new MemDb();402      const ctx = makeCtx(db);403      for (const purchaseToken of testCase.successorExists404        ? [TOKEN, "replacement"]405        : [TOKEN]) {406        const id = await recordVerifiedSubscriptionHandler(ctx, {407          projectId: PROJECT_ID as never,408          platform: "Android",409          purchaseToken,410          productId: "premium_monthly",411          purchaseState: "ENTITLED",412          subscriptionState: "SUBSCRIPTION_STATE_ACTIVE",413        });414        if (purchaseToken === testCase.erasedToken)415          await db.patch(id!, { accountErased: true });416        if (purchaseToken === testCase.boundToken)417          await bindSubscriptionToUserHandler(ctx, {418            projectId: PROJECT_ID as never,419            purchaseToken,420            userId: "linked-owner",421          });422      }423      const eventId = await seedWebhookEvent(db, {424        type: "SubscriptionStarted",425        notificationId: "erased-replacement",426        occurredAt: Date.now(),427      });428      await db.patch(eventId, {429        purchaseToken: "replacement",430        linkedPurchaseToken: TOKEN,431      });432      await applySubscriptionEventHandler(ctx, {433        projectId: PROJECT_ID as never,434        eventId: eventId as never,435      });436 437      expect(db.rows("subscriptions")).toHaveLength(1);438      const merged = db.rows("subscriptions")[0];439      expect(merged.purchaseToken).toBe("replacement");440      // The merge must not attribute the record to anyone who did not bind it,441      // and it must not revoke the binder who did.442      expect(443        db444          .rows("commerceEvents")445          .every(446            (event) =>447              event.userId === undefined || event.userId === "linked-owner",448          ),449      ).toBe(true);450      expect(451        db452          .rows("commerceEvents")453          .some((event) => event.eventType === "entitlement.revoked"),454      ).toBe(false);455      if (testCase.boundToken) {456        // A live binding on either side is a later association than the erasure,457        // so it survives and the marker does not carry.458        expect(merged.userId).toBe("linked-owner");459        expect(merged.accountErased).toBeUndefined();460      } else {461        expect(merged.userId).toBeUndefined();462        expect(merged.accountErased).toBe(true);463        // Unowned again, so the record can be associated with someone new.464        expect(465          await bindSubscriptionToUserHandler(ctx, {466            projectId: PROJECT_ID as never,467            purchaseToken: "replacement",468            userId: "new-owner",469          }),470        ).not.toBeNull();471        expect(db.rows("subscriptions")[0].accountErased).toBeUndefined();472      }473    },474  );475});476 477describe("bindSubscriptionToUser amount handling", () => {478  it("does not repeat an amount the webhook already reported", async () => {479    const db = new MemDb();480    db.seedProduct({481      projectId: PROJECT_ID,482      platform: "Android",483      productId: "premium_monthly",484      billingPeriod: "P1M",485    });486    const eventId = await seedWebhookEvent(db, {487      type: "SubscriptionStarted",488      notificationId: "message-webhook-first",489      occurredAt: 1_000,490    });491    await applySubscriptionEventHandler(makeCtx(db), {492      projectId: PROJECT_ID as never,493      eventId: eventId as never,494    });495    const afterWebhook = db496      .rows("commerceEvents")497      .filter((row) => row.amountMicros !== undefined).length;498 499    await bindSubscriptionToUserHandler(makeCtx(db), {500      projectId: PROJECT_ID as never,501      purchaseToken: TOKEN,502      userId: "user_1",503    });504 505    const priced = db506      .rows("commerceEvents")507      .filter((row) => row.amountMicros !== undefined);508    // The bind grant correlates an existing purchase to a user; it is not a509    // second billing, so the count must not move.510    expect(priced.length).toBe(afterWebhook);511    expect(db.rows("commerceEvents").at(-1)?.eventType).toBe(512      "entitlement.granted",513    );514    expect(db.rows("commerceEvents").at(-1)?.amountMicros).toBeUndefined();515  });516});517 518describe("applySubscriptionEventHandler", () => {519  beforeEach(() => {520    vi.setSystemTime(new Date("2026-01-01T00:00:00.000Z"));521  });522 523  afterEach(() => {524    vi.useRealTimers();525  });526 527  // Commerce Protocol's `whenNoPriorStoreEvent` turns on store history, not on528  // whether a record exists — a purchase learned from a client receipt but529  // never from the store still begins the story. That distinction lives here,530  // in the override, and nowhere in the state machine, so it is tested here.531  it("starts rather than recovers when a record exists with no store history", async () => {532    const db = new MemDb();533    db.seedProduct({534      projectId: PROJECT_ID,535      platform: "Android",536      productId: "premium_monthly",537      billingPeriod: "P1M",538    });539    // A client receipt created this row; no store notification ever touched it,540    // so it carries no lastEventId.541    await db.insert("subscriptions", {542      projectId: PROJECT_ID,543      platform: "Android",544      purchaseToken: TOKEN,545      productId: "premium_monthly",546      state: "Unknown",547      willRenew: true,548      updatedAt: 0,549    });550    const eventId = await seedWebhookEvent(db, {551      type: "SubscriptionStarted",552      notificationId: "message-first-store-event",553      occurredAt: 1_000,554    });555 556    await applySubscriptionEventHandler(makeCtx(db), {557      projectId: PROJECT_ID as never,558      eventId: eventId as never,559    });560    // Assert on what was emitted, not what the handler returned: the override561    // applies to the commerce event, while the return value carries the state562    // machine's own transition.563    expect(db.rows("commerceEvents").map((row) => row.eventType)).toContain(564      "subscription.started",565    );566  });567 568  it.each([["SubscriptionPriceChange"], ["SubscriptionDeferred"]])(569    "emits no commerce event when %s is the first store event for a receipt-bootstrapped row",570    async (storeEventType) => {571      // store-event-mapping.json pins whenNoPriorStoreEvent -> event: null for572      // price changes and deferrals: with no earlier store event there is no573      // baseline the event could describe.574      const db = new MemDb();575      db.seedProduct({576        projectId: PROJECT_ID,577        platform: "Android",578        productId: "premium_monthly",579        billingPeriod: "P1M",580      });581      await db.insert("subscriptions", {582        projectId: PROJECT_ID,583        platform: "Android",584        purchaseToken: TOKEN,585        productId: "premium_monthly",586        state: "Active",587        expiresAt: 9_999_999_999_999,588        willRenew: true,589        updatedAt: 0,590      });591      const eventId = await seedWebhookEvent(db, {592        type: storeEventType as never,593        notificationId: `message-${storeEventType}`,594        occurredAt: 1_000,595      });596 597      await applySubscriptionEventHandler(makeCtx(db), {598        projectId: PROJECT_ID as never,599        eventId: eventId as never,600      });601      expect(db.rows("commerceEvents")).toEqual([]);602    },603  );604 605  // Rows written before `lastEventOccurredAt` existed carry only `lastEventId`,606  // so the timestamp guard cannot judge them. Both of its fallbacks protect a607  // money path: a redelivery re-applies the transition and books it twice.608  it.each([609    ["the same event redelivered", "message-legacy", 500, "Expired"],610    ["an older event arriving late", "message-older", 200, "Expired"],611  ])(612    "drops %s against a legacy row",613    async (_name, notificationId, occurredAt, expected) => {614      const db = new MemDb();615      db.seedProduct({616        projectId: PROJECT_ID,617        platform: "Android",618        productId: "premium_monthly",619        billingPeriod: "P1M",620      });621      const priorId = await seedWebhookEvent(db, {622        type: "SubscriptionExpired",623        notificationId: "message-legacy",624        occurredAt: 500,625      });626      await db.insert("subscriptions", {627        projectId: PROJECT_ID,628        platform: "Android",629        purchaseToken: TOKEN,630        productId: "premium_monthly",631        state: "Expired",632        willRenew: false,633        lastEventId: priorId,634        updatedAt: 0,635      });636      const replayId =637        notificationId === "message-legacy"638          ? priorId639          : await seedWebhookEvent(db, {640              type: "SubscriptionStarted",641              notificationId,642              occurredAt,643            });644 645      await expect(646        applySubscriptionEventHandler(makeCtx(db), {647          projectId: PROJECT_ID as never,648          eventId: replayId as never,649        }),650      ).resolves.toMatchObject({ transition: null });651      expect(db.rows("subscriptions")).toMatchObject([{ state: expected }]);652      expect(db.rows("commerceEvents")).toEqual([]);653    },654  );655 656  it("recovers when the record already has store history", async () => {657    const db = new MemDb();658    db.seedProduct({659      projectId: PROJECT_ID,660      platform: "Android",661      productId: "premium_monthly",662      billingPeriod: "P1M",663    });664    const priorId = await seedWebhookEvent(db, {665      type: "SubscriptionExpired",666      notificationId: "message-prior",667      occurredAt: 500,668    });669    await db.insert("subscriptions", {670      projectId: PROJECT_ID,671      platform: "Android",672      purchaseToken: TOKEN,673      productId: "premium_monthly",674      state: "Expired",675      willRenew: false,676      lastEventId: priorId,677      updatedAt: 0,678    });679    const eventId = await seedWebhookEvent(db, {680      type: "SubscriptionStarted",681      notificationId: "message-after-history",682      occurredAt: 1_000,683    });684 685    await applySubscriptionEventHandler(makeCtx(db), {686      projectId: PROJECT_ID as never,687      eventId: eventId as never,688    });689    expect(db.rows("commerceEvents").map((row) => row.eventType)).toContain(690      "subscription.recovered",691    );692  });693 694  it("applies a recorded-but-unapplied event exactly once on redelivery", async () => {695    const db = new MemDb();696    db.seedProduct({697      projectId: PROJECT_ID,698      platform: "Android",699      productId: "premium_monthly",700      billingPeriod: "P1M",701    });702    const eventId = await seedWebhookEvent(db, {703      type: "SubscriptionStarted",704      notificationId: "message-a",705      occurredAt: 1_000,706    });707    const args = {708      projectId: PROJECT_ID as never,709      eventId: eventId as never,710    };711 712    await expect(713      applySubscriptionEventHandler(makeCtx(db), args),714    ).resolves.toMatchObject({ transition: "Started", active: true });715    const appliedAt = db.rows("webhookEvents")[0]?.appliedAt;716 717    await expect(718      applySubscriptionEventHandler(makeCtx(db), args),719    ).resolves.toMatchObject({ transition: null, active: true });720    expect(db.rows("webhookEvents")[0]?.appliedAt).toBe(appliedAt);721    expect(db.rows("subscriptions")).toMatchObject([722      { state: "Active", lastEventId: eventId },723    ]);724    expect(db.rows("subscriptionStats")).toMatchObject([725      { activeSubs: 1, mrrMicros: 9_990_000 },726    ]);727  });728 729  it("applies distinct same-timestamp events without replaying the old one", async () => {730    const db = new MemDb();731    db.seedProduct({732      projectId: PROJECT_ID,733      platform: "Android",734      productId: "premium_monthly",735      billingPeriod: "P1M",736    });737    const startedId = await seedWebhookEvent(db, {738      type: "SubscriptionStarted",739      notificationId: "message-a",740      occurredAt: 1_000,741    });742    const expiredId = await seedWebhookEvent(db, {743      type: "SubscriptionExpired",744      notificationId: "message-b",745      occurredAt: 1_000,746    });747 748    await applySubscriptionEventHandler(makeCtx(db), {749      projectId: PROJECT_ID as never,750      eventId: startedId as never,751    });752    await applySubscriptionEventHandler(makeCtx(db), {753      projectId: PROJECT_ID as never,754      eventId: expiredId as never,755    });756    await expect(757      applySubscriptionEventHandler(makeCtx(db), {758        projectId: PROJECT_ID as never,759        eventId: startedId as never,760      }),761    ).resolves.toMatchObject({ transition: null, active: false });762 763    expect(db.rows("subscriptions")).toMatchObject([764      { state: "Expired", lastEventId: expiredId },765    ]);766    expect(db.rows("subscriptionStats")).toMatchObject([767      { activeSubs: 0, mrrMicros: 0 },768    ]);769  });770 771  it("uses ingestion order to backfill a same-timestamp legacy event", async () => {772    const db = new MemDb();773    db.seedProduct({774      projectId: PROJECT_ID,775      platform: "Android",776      productId: "premium_monthly",777      billingPeriod: "P1M",778    });779    const startedId = await seedWebhookEvent(db, {780      type: "SubscriptionStarted",781      notificationId: "legacy-a",782      occurredAt: 1_000,783    });784    const expiredId = await seedWebhookEvent(db, {785      type: "SubscriptionExpired",786      notificationId: "legacy-b",787      occurredAt: 1_000,788    });789    await applySubscriptionEventHandler(makeCtx(db), {790      projectId: PROJECT_ID as never,791      eventId: startedId as never,792    });793    await applySubscriptionEventHandler(makeCtx(db), {794      projectId: PROJECT_ID as never,795      eventId: expiredId as never,796    });797    await db.patch(startedId, { appliedAt: undefined });798 799    await expect(800      applySubscriptionEventHandler(makeCtx(db), {801        projectId: PROJECT_ID as never,802        eventId: startedId as never,803      }),804    ).resolves.toMatchObject({ transition: null, active: false });805 806    expect(807      db.rows("webhookEvents").find((row) => row._id === startedId),808    ).toHaveProperty("appliedAt", Date.now());809    expect(db.rows("subscriptions")).toMatchObject([810      { state: "Expired", lastEventId: expiredId },811    ]);812    expect(db.rows("subscriptionStats")).toMatchObject([813      { activeSubs: 0, mrrMicros: 0 },814    ]);815  });816 817  it("keeps durable ordering after the previous webhook row is pruned", async () => {818    const db = new MemDb();819    const startedId = await seedWebhookEvent(db, {820      type: "SubscriptionStarted",821      notificationId: "newer",822      occurredAt: 2_000,823    });824    await applySubscriptionEventHandler(makeCtx(db), {825      projectId: PROJECT_ID as never,826      eventId: startedId as never,827    });828    await db.delete(startedId);829    const staleId = await seedWebhookEvent(db, {830      type: "SubscriptionExpired",831      notificationId: "older",832      occurredAt: 1_000,833    });834 835    await expect(836      applySubscriptionEventHandler(makeCtx(db), {837        projectId: PROJECT_ID as never,838        eventId: staleId as never,839      }),840    ).resolves.toMatchObject({ transition: null, active: true });841    expect(db.rows("subscriptions")).toMatchObject([842      {843        state: "Active",844        lastEventOccurredAt: 2_000,845        lastEventSourceNotificationId: "newer",846      },847    ]);848  });849 850  it("does not grant entitlement for an already-expired start event", async () => {851    const db = new MemDb();852    const eventId = await seedWebhookEvent(db, {853      type: "SubscriptionStarted",854      notificationId: "expired-start",855      occurredAt: 1_000,856    });857    await db.patch(eventId, { expiresAt: Date.now() - 1 });858 859    await expect(860      applySubscriptionEventHandler(makeCtx(db), {861        projectId: PROJECT_ID as never,862        eventId: eventId as never,863      }),864    ).resolves.toMatchObject({ transition: "Started", active: false });865    expect(db.rows("commerceEvents").map((row) => row.eventType)).toEqual([866      "subscription.started",867    ]);868  });869 870  it("records one-time events without creating subscription commerce", async () => {871    const db = new MemDb();872    const eventId = await seedWebhookEvent(db, {873      type: "SubscriptionStarted",874      notificationId: "one-time",875      occurredAt: 1_000,876    });877    await db.patch(eventId, { productKind: "one_time" });878 879    await expect(880      applySubscriptionEventHandler(makeCtx(db), {881        projectId: PROJECT_ID as never,882        eventId: eventId as never,883      }),884    ).resolves.toEqual({ transition: null, active: false });885    expect(db.rows("subscriptions")).toHaveLength(0);886    expect(db.rows("commerceEvents")).toHaveLength(0);887  });888 889  it("keeps an Apple renewal preference separate from the active product", async () => {890    const db = new MemDb();891    db.seedProduct({892      projectId: PROJECT_ID,893      platform: "IOS",894      productId: "premium_monthly",895      billingPeriod: "P1M",896    });897    const startedId = await seedWebhookEvent(db, {898      type: "SubscriptionStarted",899      notificationId: "apple-started",900      occurredAt: 1_000,901      platform: "IOS",902    });903    await applySubscriptionEventHandler(makeCtx(db), {904      projectId: PROJECT_ID as never,905      eventId: startedId as never,906    });907    const changedId = await seedWebhookEvent(db, {908      type: "SubscriptionProductChanged",909      notificationId: "apple-next-product",910      occurredAt: 2_000,911      platform: "IOS",912      productId: "premium_yearly",913    });914    await db.patch(changedId, { priceAmountMicros: 99_990_000 });915 916    await expect(917      applySubscriptionEventHandler(makeCtx(db), {918        projectId: PROJECT_ID as never,919        eventId: changedId as never,920      }),921    ).resolves.toMatchObject({ transition: "ProductChanged", active: true });922    expect(db.rows("subscriptions")).toMatchObject([923      {924        productId: "premium_monthly",925        platform: "IOS",926        priceAmountMicros: 9_990_000,927      },928    ]);929    expect(db.rows("commerceEvents").at(-1)).toMatchObject({930      eventType: "subscription.product_changed",931      productId: "premium_yearly",932      amountMicros: 99_990_000,933      subscription: { productId: "premium_monthly" },934    });935    await db.delete(changedId);936 937    await bindSubscriptionToUserHandler(makeCtx(db), {938      projectId: PROJECT_ID as never,939      purchaseToken: TOKEN,940      userId: "user-1",941    });942    expect(db.rows("commerceEvents").at(-1)).toMatchObject({943      eventType: "entitlement.granted",944      productId: "premium_monthly",945      userId: "user-1",946    });947    expect(db.rows("commerceEvents").at(-1)?.amountMicros).toBeUndefined();948 949    const renewedId = await seedWebhookEvent(db, {950      type: "SubscriptionRenewed",951      notificationId: "apple-renewed-on-new-product",952      occurredAt: 3_000,953      platform: "IOS",954      productId: "premium_yearly",955    });956    await applySubscriptionEventHandler(makeCtx(db), {957      projectId: PROJECT_ID as never,958      eventId: renewedId as never,959    });960    expect(db.rows("commerceEvents").at(-1)).toMatchObject({961      eventType: "subscription.renewed",962      productId: "premium_yearly",963      previousProductId: "premium_monthly",964      userId: "user-1",965    });966  });967 968  it("applies an Apple upgrade product immediately", async () => {969    const db = new MemDb();970    const startedId = await seedWebhookEvent(db, {971      type: "SubscriptionStarted",972      notificationId: "apple-started",973      occurredAt: 1_000,974      platform: "IOS",975    });976    await applySubscriptionEventHandler(makeCtx(db), {977      projectId: PROJECT_ID as never,978      eventId: startedId as never,979    });980    const changedId = await seedWebhookEvent(db, {981      type: "SubscriptionProductChanged",982      notificationId: "apple-upgrade",983      occurredAt: 2_000,984      platform: "IOS",985      productId: "premium_yearly",986    });987    await db.patch(changedId, { effectiveImmediately: true });988 989    await applySubscriptionEventHandler(makeCtx(db), {990      projectId: PROJECT_ID as never,991      eventId: changedId as never,992    });993 994    expect(db.rows("subscriptions")[0]).toMatchObject({995      productId: "premium_yearly",996      lastEventId: changedId,997    });998  });999 1000  it("includes the previous product for an applied Google item change", async () => {1001    const db = new MemDb();1002    const startedId = await seedWebhookEvent(db, {1003      type: "SubscriptionStarted",1004      notificationId: "google-started",1005      occurredAt: 1_000,1006    });1007    await applySubscriptionEventHandler(makeCtx(db), {1008      projectId: PROJECT_ID as never,1009      eventId: startedId as never,1010    });1011    const changedId = await seedWebhookEvent(db, {1012      type: "SubscriptionProductChanged",1013      notificationId: "google-item-change",1014      occurredAt: 2_000,1015      productId: "premium_yearly",1016    });1017    await applySubscriptionEventHandler(makeCtx(db), {1018      projectId: PROJECT_ID as never,1019      eventId: changedId as never,1020    });1021 1022    expect(db.rows("commerceEvents").at(-1)).toMatchObject({1023      eventType: "subscription.product_changed",1024      previousProductId: "premium_monthly",1025      productId: "premium_yearly",1026      subscription: { productId: "premium_yearly" },1027    });1028  });1029 1030  it("does not guess a product for a multi-item Google change", async () => {1031    const db = new MemDb();1032    const startedId = await seedWebhookEvent(db, {1033      type: "SubscriptionStarted",1034      notificationId: "google-bundle-started",1035      occurredAt: 1_000,1036    });1037    await applySubscriptionEventHandler(makeCtx(db), {1038      projectId: PROJECT_ID as never,1039      eventId: startedId as never,1040    });1041    const changedId = await db.insert("webhookEvents", {1042      projectId: PROJECT_ID,1043      type: "SubscriptionProductChanged",1044      source: "GooglePlayRealTimeDeveloperNotifications",1045      platform: "Android",1046      environment: "Production",1047      purchaseToken: TOKEN,1048      productKind: "subscription",1049      sourceNotificationId: "google-bundle-change",1050      occurredAt: 2_000,1051      receivedAt: 2_000,1052    });1053 1054    await expect(1055      applySubscriptionEventHandler(makeCtx(db), {1056        projectId: PROJECT_ID as never,1057        eventId: changedId as never,1058      }),1059    ).resolves.toMatchObject({ transition: null, active: true });1060    expect(db.rows("subscriptions")[0]).toMatchObject({1061      productId: "premium_monthly",1062      lastEventId: changedId,1063      lastEventOccurredAt: 2_000,1064    });1065    expect(db.rows("commerceEvents")).toHaveLength(1);1066 1067    const delayedId = await seedWebhookEvent(db, {1068      type: "SubscriptionExpired",1069      notificationId: "google-delayed-expiry",1070      occurredAt: 1_500,1071    });1072    await expect(1073      applySubscriptionEventHandler(makeCtx(db), {1074        projectId: PROJECT_ID as never,1075        eventId: delayedId as never,1076      }),1077    ).resolves.toMatchObject({ transition: null, active: true });1078    expect(db.rows("subscriptions")[0]).toMatchObject({1079      state: "Active",1080      lastEventId: changedId,1081    });1082  });1083 1084  it("moves a Google replacement flow onto the linked token row", async () => {1085    const db = new MemDb();1086    const startedId = await seedWebhookEvent(db, {1087      type: "SubscriptionStarted",1088      notificationId: "google-old-token",1089      occurredAt: 1_000,1090    });1091    await applySubscriptionEventHandler(makeCtx(db), {1092      projectId: PROJECT_ID as never,1093      eventId: startedId as never,1094    });1095    const changedId = await db.insert("webhookEvents", {1096      projectId: PROJECT_ID,1097      type: "SubscriptionProductChanged",1098      source: "GooglePlayRealTimeDeveloperNotifications",1099      platform: "Android",1100      environment: "Production",1101      purchaseToken: "purchase_token_2",1102      linkedPurchaseToken: TOKEN,1103      productKind: "subscription",1104      productId: "premium_yearly",1105      subscriptionState: "Active",1106      sourceNotificationId: "google-new-token",1107      occurredAt: 2_000,1108      receivedAt: 2_000,1109    });1110    await applySubscriptionEventHandler(makeCtx(db), {1111      projectId: PROJECT_ID as never,1112      eventId: changedId as never,1113    });1114 1115    expect(db.rows("subscriptions")).toHaveLength(1);1116    expect(db.rows("subscriptions")[0]).toMatchObject({1117      purchaseToken: "purchase_token_2",1118      productId: "premium_yearly",1119    });1120    expect(db.rows("commerceEvents").at(-1)).toMatchObject({1121      eventType: "subscription.product_changed",1122      previousProductId: "premium_monthly",1123      productId: "premium_yearly",1124    });1125  });1126 1127  it("classifies a linked Google purchase with a new product as a change", async () => {1128    const db = new MemDb();1129    const startedId = await seedWebhookEvent(db, {1130      type: "SubscriptionStarted",1131      notificationId: "google-linked-start-old",1132      occurredAt: 1_000,1133    });1134    await applySubscriptionEventHandler(makeCtx(db), {1135      projectId: PROJECT_ID as never,1136      eventId: startedId as never,1137    });1138    const replacementId = await db.insert("webhookEvents", {1139      projectId: PROJECT_ID,1140      type: "SubscriptionStarted",1141      source: "GooglePlayRealTimeDeveloperNotifications",1142      platform: "Android",1143      environment: "Production",1144      purchaseToken: "purchase_token_2",1145      linkedPurchaseToken: TOKEN,1146      productKind: "subscription",1147      productId: "premium_yearly",1148      subscriptionState: "Active",1149      sourceNotificationId: "google-linked-start-new",1150      occurredAt: 2_000,1151      receivedAt: 2_000,1152    });1153 1154    await expect(1155      applySubscriptionEventHandler(makeCtx(db), {1156        projectId: PROJECT_ID as never,1157        eventId: replacementId as never,1158      }),1159    ).resolves.toMatchObject({ transition: "ProductChanged", active: true });1160 1161    expect(1162      db1163        .rows("commerceEvents")1164        .filter((event) => event.sourceEventId === replacementId),1165    ).toEqual([1166      expect.objectContaining({1167        eventType: "subscription.product_changed",1168        previousProductId: "premium_monthly",1169        productId: "premium_yearly",1170      }),1171    ]);1172  });1173 1174  it("defers a same-product token handoff until the replacement renews", async () => {1175    const db = new MemDb();1176    const startedId = await seedWebhookEvent(db, {1177      type: "SubscriptionStarted",1178      notificationId: "google-deferred-start-old",1179      occurredAt: 1_000,1180    });1181    await applySubscriptionEventHandler(makeCtx(db), {1182      projectId: PROJECT_ID as never,1183      eventId: startedId as never,1184    });1185    const handoffId = await db.insert("webhookEvents", {1186      projectId: PROJECT_ID,1187      type: "SubscriptionStarted",1188      source: "GooglePlayRealTimeDeveloperNotifications",1189      platform: "Android",1190      environment: "Production",1191      purchaseToken: "purchase_token_2",1192      linkedPurchaseToken: TOKEN,1193      productKind: "subscription",1194      productId: "premium_monthly",1195      subscriptionState: "Active",1196      sourceNotificationId: "google-deferred-token-handoff",1197      occurredAt: 2_000,1198      receivedAt: 2_000,1199    });1200    await expect(1201      applySubscriptionEventHandler(makeCtx(db), {1202        projectId: PROJECT_ID as never,1203        eventId: handoffId as never,1204      }),1205    ).resolves.toMatchObject({ transition: null, active: true });1206    expect(1207      db1208        .rows("commerceEvents")1209        .filter((event) => event.sourceEventId === handoffId),1210    ).toEqual([]);1211 1212    const predecessorExpiryId = await db.insert("webhookEvents", {1213      projectId: PROJECT_ID,1214      type: "SubscriptionExpired",1215      source: "GooglePlayRealTimeDeveloperNotifications",1216      platform: "Android",1217      environment: "Production",1218      purchaseToken: TOKEN,1219      productKind: "subscription",1220      productId: "premium_monthly",1221      subscriptionState: "Expired",1222      expiresAt: 2_500,1223      sourceNotificationId: "google-deferred-predecessor-expired",1224      occurredAt: 2_500,1225      receivedAt: 2_500,1226    });1227    await expect(1228      applySubscriptionEventHandler(makeCtx(db), {1229        projectId: PROJECT_ID as never,1230        eventId: predecessorExpiryId as never,1231      }),1232    ).resolves.toMatchObject({ transition: null, active: true });1233    expect(db.rows("subscriptions")).toHaveLength(1);1234    expect(db.rows("subscriptions")[0]).toMatchObject({1235      purchaseToken: "purchase_token_2",1236      state: "Active",1237    });1238    expect(1239      db1240        .rows("commerceEvents")1241        .filter((event) => event.sourceEventId === predecessorExpiryId),1242    ).toEqual([]);1243    expect(db.rows("subscriptionTokenAliases")).toMatchObject([1244      {1245        purchaseToken: TOKEN,1246        successorPurchaseToken: "purchase_token_2",1247      },1248    ]);1249 1250    const successor = db.rows("subscriptions")[0];1251    const statsBeforeStaleVerification = structuredClone(1252      db.rows("subscriptionStats"),1253    );1254    await expect(1255      recordVerifiedSubscriptionHandler(makeCtx(db), {1256        projectId: PROJECT_ID as never,1257        platform: "Android",1258        purchaseToken: TOKEN,1259        productId: "stale_monthly",1260        purchaseState: "ENTITLED",1261        subscriptionState: "SUBSCRIPTION_STATE_ACTIVE",1262        expiresAt: 2_600,1263        willRenew: false,1264      }),1265    ).resolves.toBe(successor._id);1266    expect(db.rows("subscriptions")).toMatchObject([1267      {1268        _id: successor._id,1269        purchaseToken: "purchase_token_2",1270        productId: "premium_monthly",1271        state: "Active",1272      },1273    ]);1274    expect(db.rows("subscriptionStats")).toEqual(statsBeforeStaleVerification);1275 1276    await expect(1277      bindSubscriptionToUserHandler(makeCtx(db), {1278        projectId: PROJECT_ID as never,1279        purchaseToken: TOKEN,1280        userId: "user-after-handoff",1281      }),1282    ).resolves.toBe(successor._id);1283    expect(db.rows("subscriptions")).toMatchObject([1284      {1285        _id: successor._id,1286        purchaseToken: "purchase_token_2",1287        userId: "user-after-handoff",1288      },1289    ]);1290    expect(db.rows("subscriptions")).toHaveLength(1);1291 1292    const renewalId = await db.insert("webhookEvents", {1293      projectId: PROJECT_ID,1294      type: "SubscriptionRenewed",1295      source: "GooglePlayRealTimeDeveloperNotifications",1296      platform: "Android",1297      environment: "Production",1298      purchaseToken: "purchase_token_2",1299      linkedPurchaseToken: TOKEN,1300      productKind: "subscription",1301      productId: "premium_yearly",1302      subscriptionState: "Active",1303      sourceNotificationId: "google-deferred-first-renewal",1304      occurredAt: 3_000,1305      receivedAt: 3_000,1306    });1307    await applySubscriptionEventHandler(makeCtx(db), {1308      projectId: PROJECT_ID as never,1309      eventId: renewalId as never,1310    });1311 1312    expect(db.rows("subscriptions")[0]).toMatchObject({1313      purchaseToken: "purchase_token_2",1314      productId: "premium_yearly",1315    });1316    expect(1317      db1318        .rows("commerceEvents")1319        .filter((event) => event.sourceEventId === renewalId),1320    ).toEqual([1321      expect.objectContaining({1322        eventType: "subscription.renewed",1323        previousProductId: "premium_monthly",1324        productId: "premium_yearly",1325      }),1326    ]);1327  });1328 1329  it("resolves predecessor aliases beyond 32 replacement hops", async () => {1330    const db = new MemDb();1331    const hopCount = 40;1332    for (let index = 0; index < hopCount; index += 1) {1333      await db.insert("subscriptionTokenAliases", {1334        projectId: PROJECT_ID,1335        purchaseToken: `token_${index}`,1336        successorPurchaseToken: `token_${index + 1}`,1337        predecessorProductId:1338          index === 0 ? "premium_monthly" : "premium_yearly",1339        createdAt: index,1340        updatedAt: index,1341      });1342    }1343    const subscriptionId = await db.insert("subscriptions", {1344      projectId: PROJECT_ID,1345      purchaseToken: `token_${hopCount}`,1346      productKind: "subscription",1347      productId: "premium_yearly",1348      platform: "Android",1349      state: "Active",1350      expiresAt: 1_900_000_000_000,1351      willRenew: true,1352      startedAt: 1,1353      updatedAt: 1,1354    });1355    await db.insert("subscriptions", {1356      projectId: PROJECT_ID,1357      purchaseToken: "token_20",1358      productKind: "subscription",1359      productId: "stale_intermediate",1360      platform: "Android",1361      state: "Expired",1362      expiresAt: 2_000,1363      willRenew: false,1364      startedAt: 1,1365      updatedAt: 1,1366    });1367    const expiredId = await db.insert("webhookEvents", {1368      projectId: PROJECT_ID,1369      type: "SubscriptionExpired",1370      source: "GooglePlayRealTimeDeveloperNotifications",1371      platform: "Android",1372      environment: "Production",1373      purchaseToken: "token_0",1374      productKind: "subscription",1375      productId: "premium_monthly",1376      sourceNotificationId: "old-chain-expired",1377      occurredAt: 2_000,1378      receivedAt: 2_000,1379    });1380 1381    await expect(1382      getSourceProductIdByTokenHandler(makeCtx(db), {1383        projectId: PROJECT_ID as never,1384        purchaseToken: "token_0",1385      }),1386    ).resolves.toBe("premium_monthly");1387    await expect(1388      getCurrentProductIdByTokenHandler(makeCtx(db), {1389        projectId: PROJECT_ID as never,1390        purchaseToken: "token_0",1391      }),1392    ).resolves.toBe("premium_yearly");1393 1394    await expect(1395      applySubscriptionEventHandler(makeCtx(db), {1396        projectId: PROJECT_ID as never,1397        eventId: expiredId as never,1398      }),1399    ).resolves.toEqual({1400      transition: null,1401      active: true,1402      subscriptionId,1403    });1404    expect(db.rows("subscriptions")).toHaveLength(2);1405    expect(1406      db.rows("subscriptions").find((row) => row._id === subscriptionId),1407    ).toMatchObject({1408      purchaseToken: `token_${hopCount}`,1409      productId: "premium_yearly",1410      state: "Active",1411    });1412    expect(db.rows("commerceEvents")).toEqual([]);1413  });1414 1415  it("emits predecessor refunds and revocations without deactivating the successor", async () => {1416    const db = new MemDb();1417    const subscriptionId = await db.insert("subscriptions", {1418      projectId: PROJECT_ID,1419      purchaseToken: "current_token",1420      productKind: "subscription",1421      productId: "premium_yearly",1422      platform: "Android",1423      state: "Active",1424      expiresAt: 1_900_000_000_000,1425      willRenew: true,1426      startedAt: 1,1427      updatedAt: 1,1428    });1429    await db.insert("subscriptionTokenAliases", {1430      projectId: PROJECT_ID,1431      purchaseToken: "old_token",1432      successorPurchaseToken: "current_token",1433      predecessorProductId: "premium_monthly",1434      createdAt: 1,1435      updatedAt: 1,1436    });1437 1438    const cases = [1439      ["PurchaseRefunded", "Refunded", "subscription.refunded"],1440      ["SubscriptionRevoked", "Revoked", "subscription.revoked"],1441    ] as const;1442    for (const [type, transition, eventType] of cases) {1443      const eventId = await db.insert("webhookEvents", {1444        projectId: PROJECT_ID,1445        type,1446        source: "GooglePlayRealTimeDeveloperNotifications",1447        platform: "Android",1448        environment: "Production",1449        purchaseToken: "old_token",1450        productKind: "subscription",1451        productId: "premium_monthly",1452        sourceNotificationId: `old-token-${type}`,1453        occurredAt: 2_000,1454        receivedAt: 2_000,1455      });1456      await expect(1457        applySubscriptionEventHandler(makeCtx(db), {1458          projectId: PROJECT_ID as never,1459          eventId: eventId as never,1460        }),1461      ).resolves.toEqual({ transition, active: true, subscriptionId });1462      expect(1463        db1464          .rows("commerceEvents")1465          .filter((event) => event.sourceEventId === eventId)1466          .map((event) => [1467            event.eventType,1468            event.productId,1469            (event.subscription as { state?: string } | undefined)?.state,1470            event.entitlementActive,1471          ]),1472      ).toEqual([[eventType, "premium_monthly", transition, false]]);1473    }1474    expect(db.rows("subscriptions")).toMatchObject([1475      {1476        purchaseToken: "current_token",1477        productId: "premium_yearly",1478        state: "Active",1479      },1480    ]);1481  });1482 1483  it("classifies a same-product linked prepaid extension as a renewal", async () => {1484    const db = new MemDb();1485    const startedId = await seedWebhookEvent(db, {1486      type: "SubscriptionStarted",1487      notificationId: "google-prepaid-start",1488      occurredAt: 1_000,1489    });1490    await applySubscriptionEventHandler(makeCtx(db), {1491      projectId: PROJECT_ID as never,1492      eventId: startedId as never,1493    });1494    const topUpId = await db.insert("webhookEvents", {1495      projectId: PROJECT_ID,1496      type: "SubscriptionStarted",1497      source: "GooglePlayRealTimeDeveloperNotifications",1498      platform: "Android",1499      environment: "Production",1500      purchaseToken: "purchase_token_2",1501      linkedPurchaseToken: TOKEN,1502      productKind: "subscription",1503      productId: "premium_monthly",1504      subscriptionState: "Active",1505      expiresAt: 1_900_000_000_000,1506      willRenew: false,1507      sourceNotificationId: "google-prepaid-top-up",1508      occurredAt: 2_000,1509      receivedAt: 2_000,1510    });1511 1512    await expect(1513      applySubscriptionEventHandler(makeCtx(db), {1514        projectId: PROJECT_ID as never,1515        eventId: topUpId as never,1516      }),1517    ).resolves.toMatchObject({ transition: "Renewed", active: true });1518    expect(1519      db1520        .rows("commerceEvents")1521        .filter((event) => event.sourceEventId === topUpId),1522    ).toEqual([1523      expect.objectContaining({1524        eventType: "subscription.renewed",1525        productId: "premium_monthly",1526        subscription: expect.objectContaining({1527          expiresAt: 1_900_000_000_000,1528          willRenew: false,1529        }),1530      }),1531    ]);1532    expect(1533      db.rows("commerceEvents").find((event) => event.sourceEventId === topUpId)1534        ?.subscription,1535    ).not.toHaveProperty("renewsAt");1536    expect(db.rows("subscriptions")[0]).toMatchObject({1537      willRenew: false,1538      expiresAt: 1_900_000_000_000,1539    });1540    expect(db.rows("subscriptions")[0]?.renewsAt).toBeUndefined();1541  });1542 1543  it("classifies an active same-product linked resubscribe as uncanceled", async () => {1544    const db = new MemDb();1545    const startedId = await seedWebhookEvent(db, {1546      type: "SubscriptionStarted",1547      notificationId: "google-resubscribe-start",1548      occurredAt: 1_000,1549    });1550    await applySubscriptionEventHandler(makeCtx(db), {1551      projectId: PROJECT_ID as never,1552      eventId: startedId as never,1553    });1554    const canceledId = await db.insert("webhookEvents", {1555      projectId: PROJECT_ID,1556      type: "SubscriptionCanceled",1557      source: "GooglePlayRealTimeDeveloperNotifications",1558      platform: "Android",1559      environment: "Production",1560      purchaseToken: TOKEN,1561      productKind: "subscription",1562      productId: "premium_monthly",1563      subscriptionState: "Active",1564      expiresAt: 1_800_000_000_000,1565      willRenew: false,1566      sourceNotificationId: "google-resubscribe-canceled",1567      occurredAt: 2_000,1568      receivedAt: 2_000,1569    });1570    await applySubscriptionEventHandler(makeCtx(db), {1571      projectId: PROJECT_ID as never,1572      eventId: canceledId as never,1573    });1574    const resubscribedId = await db.insert("webhookEvents", {1575      projectId: PROJECT_ID,1576      type: "SubscriptionStarted",1577      source: "GooglePlayRealTimeDeveloperNotifications",1578      platform: "Android",1579      environment: "Production",1580      purchaseToken: "purchase_token_2",1581      linkedPurchaseToken: TOKEN,1582      productKind: "subscription",1583      productId: "premium_monthly",1584      subscriptionState: "Active",1585      expiresAt: 1_800_000_000_000,1586      willRenew: true,1587      sourceNotificationId: "google-resubscribe-linked",1588      occurredAt: 3_000,1589      receivedAt: 3_000,1590    });1591 1592    await expect(1593      applySubscriptionEventHandler(makeCtx(db), {1594        projectId: PROJECT_ID as never,1595        eventId: resubscribedId as never,1596      }),1597    ).resolves.toMatchObject({ transition: "Uncanceled", active: true });1598    expect(1599      db1600        .rows("commerceEvents")1601        .filter((event) => event.sourceEventId === resubscribedId)1602        .map((event) => event.eventType),1603    ).toEqual(["subscription.uncanceled"]);1604  });1605 1606  it("recovers a linked inactive predecessor regardless of verification order", async () => {1607    const db = new MemDb();1608    const startedId = await seedWebhookEvent(db, {1609      type: "SubscriptionStarted",1610      notificationId: "google-order-start",1611      occurredAt: 1_000,1612    });1613    await applySubscriptionEventHandler(makeCtx(db), {1614      projectId: PROJECT_ID as never,1615      eventId: startedId as never,1616    });1617    const expiredId = await seedWebhookEvent(db, {1618      type: "SubscriptionExpired",1619      notificationId: "google-order-expired",1620      occurredAt: 2_000,1621    });1622    await applySubscriptionEventHandler(makeCtx(db), {1623      projectId: PROJECT_ID as never,1624      eventId: expiredId as never,1625    });1626    await recordVerifiedSubscriptionHandler(makeCtx(db), {1627      projectId: PROJECT_ID as never,1628      platform: "Android",1629      purchaseToken: "purchase_token_2",1630      productId: "premium_monthly",1631      purchaseState: "ENTITLED",1632      subscriptionState: "SUBSCRIPTION_STATE_ACTIVE",1633      expiresAt: 1_900_000_000_000,1634    });1635    const recoveredId = await db.insert("webhookEvents", {1636      projectId: PROJECT_ID,1637      type: "SubscriptionStarted",1638      source: "GooglePlayRealTimeDeveloperNotifications",1639      platform: "Android",1640      environment: "Production",1641      purchaseToken: "purchase_token_2",1642      linkedPurchaseToken: TOKEN,1643      productKind: "subscription",1644      productId: "premium_monthly",1645      subscriptionState: "Active",1646      expiresAt: 1_900_000_000_000,1647      willRenew: true,1648      sourceNotificationId: "google-order-linked",1649      occurredAt: 3_000,1650      receivedAt: 3_000,1651    });1652 1653    await expect(1654      applySubscriptionEventHandler(makeCtx(db), {1655        projectId: PROJECT_ID as never,1656        eventId: recoveredId as never,1657      }),1658    ).resolves.toMatchObject({ transition: "Recovered", active: true });1659    expect(1660      db1661        .rows("commerceEvents")1662        .filter((event) => event.sourceEventId === recoveredId)1663        .map((event) => event.eventType),1664    ).toEqual(["subscription.recovered"]);1665  });1666 1667  it("recovers an inactive predecessor through a linked Google purchase", async () => {1668    const db = new MemDb();1669    const startedId = await seedWebhookEvent(db, {1670      type: "SubscriptionStarted",1671      notificationId: "google-linked-inactive-start",1672      occurredAt: 1_000,1673    });1674    await applySubscriptionEventHandler(makeCtx(db), {1675      projectId: PROJECT_ID as never,1676      eventId: startedId as never,1677    });1678    const expiredId = await seedWebhookEvent(db, {1679      type: "SubscriptionExpired",1680      notificationId: "google-linked-inactive-expired",1681      occurredAt: 2_000,1682    });1683    await applySubscriptionEventHandler(makeCtx(db), {1684      projectId: PROJECT_ID as never,1685      eventId: expiredId as never,1686    });1687    const recoveredId = await db.insert("webhookEvents", {1688      projectId: PROJECT_ID,1689      type: "SubscriptionStarted",1690      source: "GooglePlayRealTimeDeveloperNotifications",1691      platform: "Android",1692      environment: "Production",1693      purchaseToken: "purchase_token_2",1694      linkedPurchaseToken: TOKEN,1695      productKind: "subscription",1696      productId: "premium_monthly",1697      subscriptionState: "Active",1698      sourceNotificationId: "google-linked-inactive-repurchase",1699      occurredAt: 3_000,1700      receivedAt: 3_000,1701    });1702 1703    await expect(1704      applySubscriptionEventHandler(makeCtx(db), {1705        projectId: PROJECT_ID as never,1706        eventId: recoveredId as never,1707      }),1708    ).resolves.toMatchObject({ transition: "Recovered", active: true });1709    expect(1710      db1711        .rows("commerceEvents")1712        .filter((event) => event.sourceEventId === recoveredId)1713        .map((event) => event.eventType),1714    ).toEqual(["subscription.recovered"]);1715  });1716 1717  it("applies a delayed replacement after the predecessor expires", async () => {1718    const db = new MemDb();1719    const startedId = await seedWebhookEvent(db, {1720      type: "SubscriptionStarted",1721      notificationId: "google-predecessor-started",1722      occurredAt: 1_000,1723    });1724    await applySubscriptionEventHandler(makeCtx(db), {1725      projectId: PROJECT_ID as never,1726      eventId: startedId as never,1727    });1728    const expiredId = await seedWebhookEvent(db, {1729      type: "SubscriptionExpired",1730      notificationId: "google-predecessor-expired",1731      occurredAt: 3_000,1732    });1733    await applySubscriptionEventHandler(makeCtx(db), {1734      projectId: PROJECT_ID as never,1735      eventId: expiredId as never,1736    });1737    const changedId = await db.insert("webhookEvents", {1738      projectId: PROJECT_ID,1739      type: "SubscriptionProductChanged",1740      source: "GooglePlayRealTimeDeveloperNotifications",1741      platform: "Android",1742      environment: "Production",1743      purchaseToken: "purchase_token_2",1744      linkedPurchaseToken: TOKEN,1745      productKind: "subscription",1746      productId: "premium_yearly",1747      subscriptionState: "Active",1748      sourceNotificationId: "google-delayed-replacement",1749      occurredAt: 2_000,1750      receivedAt: 4_000,1751    });1752 1753    await applySubscriptionEventHandler(makeCtx(db), {1754      projectId: PROJECT_ID as never,1755      eventId: changedId as never,1756    });1757 1758    expect(db.rows("subscriptions")).toHaveLength(1);1759    expect(db.rows("subscriptions")[0]).toMatchObject({1760      purchaseToken: "purchase_token_2",1761      productId: "premium_yearly",1762      state: "Active",1763      lastEventId: changedId,1764    });1765    expect(1766      db.rows("webhookEvents").find((row) => row._id === changedId),1767    ).toHaveProperty("appliedAt");1768    expect(db.rows("commerceEvents")).toContainEqual(1769      expect.objectContaining({1770        eventType: "subscription.product_changed",1771        previousProductId: "premium_monthly",1772        productId: "premium_yearly",1773      }),1774    );1775  });1776 1777  it("keeps verification-first replacement state over a later predecessor expiry", async () => {1778    const db = new MemDb();1779    db.seedProduct({1780      projectId: PROJECT_ID,1781      platform: "Android",1782      productId: "premium_monthly",1783      billingPeriod: "P1M",1784    });1785    db.seedProduct({1786      projectId: PROJECT_ID,1787      platform: "Android",1788      productId: "premium_yearly",1789      billingPeriod: "P1Y",1790    });1791    const startedId = await seedWebhookEvent(db, {1792      type: "SubscriptionStarted",1793      notificationId: "google-old-token",1794      occurredAt: 1_000,1795    });1796    await applySubscriptionEventHandler(makeCtx(db), {1797      projectId: PROJECT_ID as never,1798      eventId: startedId as never,1799    });1800    await bindSubscriptionToUserHandler(makeCtx(db), {1801      projectId: PROJECT_ID as never,1802      purchaseToken: TOKEN,1803      userId: "user_1",1804    });1805    await recordVerifiedSubscriptionHandler(makeCtx(db), {1806      projectId: PROJECT_ID as never,1807      platform: "Android",1808      purchaseToken: "purchase_token_2",1809      productId: "premium_yearly",1810      purchaseState: "ENTITLED",1811      subscriptionState: "SUBSCRIPTION_STATE_ACTIVE",1812    });1813    expect(db.rows("subscriptions")).toHaveLength(2);1814    expect(1815      db1816        .rows("subscriptionStats")1817        .reduce((total, row) => total + Number(row.activeSubs), 0),1818    ).toBe(2);1819 1820    const expiredId = await db.insert("webhookEvents", {1821      projectId: PROJECT_ID,1822      type: "SubscriptionExpired",1823      source: "GooglePlayRealTimeDeveloperNotifications",1824      platform: "Android",1825      environment: "Production",1826      purchaseToken: TOKEN,1827      productKind: "subscription",1828      productId: "premium_monthly",1829      subscriptionState: "Expired",1830      sourceNotificationId: "google-old-token-expired",1831      occurredAt: 3_000,1832      receivedAt: 3_000,1833    });1834    await applySubscriptionEventHandler(makeCtx(db), {1835      projectId: PROJECT_ID as never,1836      eventId: expiredId as never,1837    });1838 1839    const changedId = await db.insert("webhookEvents", {1840      projectId: PROJECT_ID,1841      type: "SubscriptionProductChanged",1842      source: "GooglePlayRealTimeDeveloperNotifications",1843      platform: "Android",1844      environment: "Production",1845      purchaseToken: "purchase_token_2",1846      linkedPurchaseToken: TOKEN,1847      productKind: "subscription",1848      productId: "premium_yearly",1849      subscriptionState: "Active",1850      sourceNotificationId: "google-new-token",1851      occurredAt: 2_000,1852      receivedAt: 2_000,1853    });1854    await applySubscriptionEventHandler(makeCtx(db), {1855      projectId: PROJECT_ID as never,1856      eventId: changedId as never,1857    });1858 1859    expect(db.rows("subscriptions")).toHaveLength(1);1860    expect(db.rows("subscriptions")[0]).toMatchObject({1861      purchaseToken: "purchase_token_2",1862      productId: "premium_yearly",1863      userId: "user_1",1864      state: "Active",1865      lastEventId: changedId,1866    });1867    expect(1868      db1869        .rows("subscriptionStats")1870        .reduce((total, row) => total + Number(row.activeSubs), 0),1871    ).toBe(1);1872  });1873 1874  it("preserves active predecessor semantics after replacement verification", async () => {1875    const db = new MemDb();1876    const startedId = await seedWebhookEvent(db, {1877      type: "SubscriptionStarted",1878      notificationId: "google-active-predecessor",1879      occurredAt: 1_000,1880    });1881    await applySubscriptionEventHandler(makeCtx(db), {1882      projectId: PROJECT_ID as never,1883      eventId: startedId as never,1884    });1885    await recordVerifiedSubscriptionHandler(makeCtx(db), {1886      projectId: PROJECT_ID as never,1887      platform: "Android",1888      purchaseToken: "purchase_token_2",1889      productId: "premium_yearly",1890      purchaseState: "ENTITLED",1891      subscriptionState: "SUBSCRIPTION_STATE_ACTIVE",1892    });1893    const changedId = await db.insert("webhookEvents", {1894      projectId: PROJECT_ID,1895      type: "SubscriptionProductChanged",1896      source: "GooglePlayRealTimeDeveloperNotifications",1897      platform: "Android",1898      environment: "Production",1899      purchaseToken: "purchase_token_2",1900      linkedPurchaseToken: TOKEN,1901      productKind: "subscription",1902      productId: "premium_yearly",1903      subscriptionState: "Active",1904      sourceNotificationId: "google-verified-replacement",1905      occurredAt: 2_000,1906      receivedAt: 2_000,1907    });1908 1909    await applySubscriptionEventHandler(makeCtx(db), {1910      projectId: PROJECT_ID as never,1911      eventId: changedId as never,1912    });1913 1914    expect(db.rows("subscriptions")).toHaveLength(1);1915    expect(db.rows("subscriptions")[0]).toMatchObject({1916      purchaseToken: "purchase_token_2",1917      productId: "premium_yearly",1918      state: "Active",1919      lastEventId: changedId,1920    });1921    expect(1922      db1923        .rows("commerceEvents")1924        .filter((event) => event.sourceEventId === changedId),1925    ).toEqual([1926      expect.objectContaining({1927        eventType: "subscription.product_changed",1928        previousProductId: "premium_monthly",1929        productId: "premium_yearly",1930      }),1931    ]);1932  });1933 1934  it("emits a verified replacement when multi-item enrichment omits the product", async () => {1935    const db = new MemDb();1936    const startedId = await seedWebhookEvent(db, {1937      type: "SubscriptionStarted",1938      notificationId: "google-active-predecessor-ambiguous",1939      occurredAt: 1_000,1940    });1941    await applySubscriptionEventHandler(makeCtx(db), {1942      projectId: PROJECT_ID as never,1943      eventId: startedId as never,1944    });1945    await recordVerifiedSubscriptionHandler(makeCtx(db), {1946      projectId: PROJECT_ID as never,1947      platform: "Android",1948      purchaseToken: "purchase_token_2",1949      productId: "premium_yearly",1950      purchaseState: "ENTITLED",1951      subscriptionState: "SUBSCRIPTION_STATE_ACTIVE",1952    });1953    const changedId = await db.insert("webhookEvents", {1954      projectId: PROJECT_ID,1955      type: "SubscriptionProductChanged",1956      source: "GooglePlayRealTimeDeveloperNotifications",1957      platform: "Android",1958      environment: "Production",1959      purchaseToken: "purchase_token_2",1960      linkedPurchaseToken: TOKEN,1961      productKind: "subscription",1962      subscriptionState: "Active",1963      sourceNotificationId: "google-verified-ambiguous-replacement",1964      occurredAt: 2_000,1965      receivedAt: 2_000,1966    });1967 1968    await applySubscriptionEventHandler(makeCtx(db), {1969      projectId: PROJECT_ID as never,1970      eventId: changedId as never,1971    });1972 1973    expect(db.rows("subscriptions")).toHaveLength(1);1974    expect(db.rows("subscriptions")[0]).toMatchObject({1975      purchaseToken: "purchase_token_2",1976      productId: "premium_yearly",1977      state: "Active",1978      lastEventId: changedId,1979    });1980    expect(1981      db1982        .rows("commerceEvents")1983        .filter((event) => event.sourceEventId === changedId),1984    ).toEqual([1985      expect.objectContaining({1986        eventType: "subscription.product_changed",1987        previousProductId: "premium_monthly",1988        productId: "premium_yearly",1989      }),1990    ]);1991  });1992 1993  it("rejects linked Google tokens bound to different users", async () => {1994    const db = new MemDb();1995    const startedId = await seedWebhookEvent(db, {1996      type: "SubscriptionStarted",1997      notificationId: "google-old-token",1998      occurredAt: 1_000,1999    });2000    await applySubscriptionEventHandler(makeCtx(db), {2001      projectId: PROJECT_ID as never,2002      eventId: startedId as never,2003    });2004    await bindSubscriptionToUserHandler(makeCtx(db), {2005      projectId: PROJECT_ID as never,2006      purchaseToken: TOKEN,2007      userId: "user_1",2008    });2009    await recordVerifiedSubscriptionHandler(makeCtx(db), {2010      projectId: PROJECT_ID as never,2011      platform: "Android",2012      purchaseToken: "purchase_token_2",2013      productId: "premium_yearly",2014      purchaseState: "ENTITLED",2015      subscriptionState: "SUBSCRIPTION_STATE_ACTIVE",2016    });2017    await bindSubscriptionToUserHandler(makeCtx(db), {2018      projectId: PROJECT_ID as never,2019      purchaseToken: "purchase_token_2",2020      userId: "user_2",2021    });2022    const changedId = await db.insert("webhookEvents", {2023      projectId: PROJECT_ID,2024      type: "SubscriptionProductChanged",2025      source: "GooglePlayRealTimeDeveloperNotifications",2026      platform: "Android",2027      environment: "Production",2028      purchaseToken: "purchase_token_2",2029      linkedPurchaseToken: TOKEN,2030      productKind: "subscription",2031      productId: "premium_yearly",2032      sourceNotificationId: "google-conflicting-token",2033      occurredAt: 2_000,2034      receivedAt: 2_000,2035    });2036 2037    await expect(2038      applySubscriptionEventHandler(makeCtx(db), {2039        projectId: PROJECT_ID as never,2040        eventId: changedId as never,2041      }),2042    ).rejects.toThrow("different users");2043    expect(db.rows("subscriptions")).toHaveLength(2);2044  });2045 2046  it("keeps a newer store-governed replacement row over an older linked event", async () => {2047    const db = new MemDb();2048    const oldStartedId = await seedWebhookEvent(db, {2049      type: "SubscriptionStarted",2050      notificationId: "google-old-token",2051      occurredAt: 1_000,2052    });2053    await applySubscriptionEventHandler(makeCtx(db), {2054      projectId: PROJECT_ID as never,2055      eventId: oldStartedId as never,2056    });2057    const newStartedId = await db.insert("webhookEvents", {2058      projectId: PROJECT_ID,2059      type: "SubscriptionStarted",2060      source: "GooglePlayRealTimeDeveloperNotifications",2061      platform: "Android",2062      environment: "Production",2063      purchaseToken: "purchase_token_2",2064      productKind: "subscription",2065      productId: "premium_monthly",2066      subscriptionState: "Active",2067      sourceNotificationId: "google-new-token-started",2068      occurredAt: 3_000,2069      receivedAt: 3_000,2070    });2071    await applySubscriptionEventHandler(makeCtx(db), {2072      projectId: PROJECT_ID as never,2073      eventId: newStartedId as never,2074    });2075    const olderLinkedId = await db.insert("webhookEvents", {2076      projectId: PROJECT_ID,2077      type: "SubscriptionProductChanged",2078      source: "GooglePlayRealTimeDeveloperNotifications",2079      platform: "Android",2080      environment: "Production",2081      purchaseToken: "purchase_token_2",2082      linkedPurchaseToken: TOKEN,2083      productKind: "subscription",2084      productId: "premium_yearly",2085      subscriptionState: "Active",2086      sourceNotificationId: "google-older-linked-event",2087      occurredAt: 2_000,2088      receivedAt: 4_000,2089    });2090 2091    await applySubscriptionEventHandler(makeCtx(db), {2092      projectId: PROJECT_ID as never,2093      eventId: olderLinkedId as never,2094    });2095 2096    expect(db.rows("subscriptions")).toHaveLength(1);2097    expect(db.rows("subscriptions")[0]).toMatchObject({2098      purchaseToken: "purchase_token_2",2099      productId: "premium_monthly",2100      lastEventId: newStartedId,2101    });2102    expect(2103      db.rows("webhookEvents").find((row) => row._id === olderLinkedId),2104    ).toHaveProperty("appliedAt");2105    expect(db.rows("subscriptionTokenAliases")).toMatchObject([2106      {2107        purchaseToken: TOKEN,2108        successorPurchaseToken: "purchase_token_2",2109      },2110    ]);2111 2112    const latePredecessorExpiryId = await db.insert("webhookEvents", {2113      projectId: PROJECT_ID,2114      type: "SubscriptionExpired",2115      source: "GooglePlayRealTimeDeveloperNotifications",2116      platform: "Android",2117      environment: "Production",2118      purchaseToken: TOKEN,2119      productKind: "subscription",2120      productId: "premium_monthly",2121      subscriptionState: "Expired",2122      sourceNotificationId: "google-late-predecessor-expired",2123      occurredAt: 4_000,2124      receivedAt: 4_000,2125    });2126    await expect(2127      applySubscriptionEventHandler(makeCtx(db), {2128        projectId: PROJECT_ID as never,2129        eventId: latePredecessorExpiryId as never,2130      }),2131    ).resolves.toMatchObject({ transition: null, active: true });2132    expect(db.rows("subscriptions")).toHaveLength(1);2133    expect(2134      db2135        .rows("commerceEvents")2136        .filter((event) => event.sourceEventId === latePredecessorExpiryId),2137    ).toEqual([]);2138  });2139 2140  it("keeps current-token state when the predecessor expires later", async () => {2141    const db = new MemDb();2142    const oldStartedId = await seedWebhookEvent(db, {2143      type: "SubscriptionStarted",2144      notificationId: "google-predecessor-started",2145      occurredAt: 1_000,2146    });2147    await applySubscriptionEventHandler(makeCtx(db), {2148      projectId: PROJECT_ID as never,2149      eventId: oldStartedId as never,2150    });2151    const replacementStartedId = await db.insert("webhookEvents", {2152      projectId: PROJECT_ID,2153      type: "SubscriptionStarted",2154      source: "GooglePlayRealTimeDeveloperNotifications",2155      platform: "Android",2156      environment: "Production",2157      purchaseToken: "purchase_token_2",2158      productKind: "subscription",2159      productId: "premium_monthly",2160      subscriptionState: "Active",2161      sourceNotificationId: "google-replacement-started",2162      occurredAt: 2_000,2163      receivedAt: 2_000,2164    });2165    await applySubscriptionEventHandler(makeCtx(db), {2166      projectId: PROJECT_ID as never,2167      eventId: replacementStartedId as never,2168    });2169    const predecessorExpiredId = await db.insert("webhookEvents", {2170      projectId: PROJECT_ID,2171      type: "SubscriptionExpired",2172      source: "GooglePlayRealTimeDeveloperNotifications",2173      platform: "Android",2174      environment: "Production",2175      purchaseToken: TOKEN,2176      productKind: "subscription",2177      productId: "premium_monthly",2178      subscriptionState: "Expired",2179      sourceNotificationId: "google-predecessor-expired",2180      occurredAt: 3_000,2181      receivedAt: 3_000,2182    });2183    await applySubscriptionEventHandler(makeCtx(db), {2184      projectId: PROJECT_ID as never,2185      eventId: predecessorExpiredId as never,2186    });2187    const linkedId = await db.insert("webhookEvents", {2188      projectId: PROJECT_ID,2189      type: "SubscriptionProductChanged",2190      source: "GooglePlayRealTimeDeveloperNotifications",2191      platform: "Android",2192      environment: "Production",2193      purchaseToken: "purchase_token_2",2194      linkedPurchaseToken: TOKEN,2195      productKind: "subscription",2196      productId: "premium_yearly",2197      subscriptionState: "Active",2198      sourceNotificationId: "google-linked-replacement",2199      occurredAt: 4_000,2200      receivedAt: 4_000,2201    });2202 2203    await applySubscriptionEventHandler(makeCtx(db), {2204      projectId: PROJECT_ID as never,2205      eventId: linkedId as never,2206    });2207 2208    expect(db.rows("subscriptions")).toHaveLength(1);2209    expect(db.rows("subscriptions")[0]).toMatchObject({2210      purchaseToken: "purchase_token_2",2211      productId: "premium_yearly",2212      state: "Active",2213      lastEventId: linkedId,2214    });2215  });2216 2217  it("moves a multi-item Google replacement token without guessing a product", async () => {2218    const db = new MemDb();2219    const startedId = await seedWebhookEvent(db, {2220      type: "SubscriptionStarted",2221      notificationId: "google-old-token",2222      occurredAt: 1_000,2223    });2224    await applySubscriptionEventHandler(makeCtx(db), {2225      projectId: PROJECT_ID as never,2226      eventId: startedId as never,2227    });2228    const changedId = await db.insert("webhookEvents", {2229      projectId: PROJECT_ID,2230      type: "SubscriptionProductChanged",2231      source: "GooglePlayRealTimeDeveloperNotifications",2232      platform: "Android",2233      environment: "Production",2234      purchaseToken: "purchase_token_2",2235      linkedPurchaseToken: TOKEN,2236      productKind: "subscription",2237      sourceNotificationId: "google-bundle-change",2238      occurredAt: 2_000,2239      receivedAt: 2_000,2240    });2241 2242    await applySubscriptionEventHandler(makeCtx(db), {2243      projectId: PROJECT_ID as never,2244      eventId: changedId as never,2245    });2246 2247    expect(db.rows("subscriptions")).toHaveLength(1);2248    expect(db.rows("subscriptions")[0]).toMatchObject({2249      purchaseToken: "purchase_token_2",2250      productId: "premium_monthly",2251      lastEventId: changedId,2252    });2253    expect(db.rows("commerceEvents")).toHaveLength(1);2254  });2255});2256 2257describe("buildVerifiedSubscriptionSnapshot", () => {2258  it("bootstraps an active subscription from an entitled Google verification", () => {2259    const snapshot = buildVerifiedSubscriptionSnapshot({2260      platform: "Android",2261      productId: "premium_monthly",2262      purchaseState: HarmonizedPurchaseState.ENTITLED,2263      subscriptionState: "SUBSCRIPTION_STATE_ACTIVE",2264      expiresAt: 2_000_000_000_000,2265    });2266 2267    expect(snapshot).toEqual({2268      productId: "premium_monthly",2269      state: "Active",2270      expiresAt: 2_000_000_000_000,2271      renewsAt: undefined,2272      willRenew: true,2273      cancellationReason: undefined,2274      clearCancellationReason: true,2275      currency: undefined,2276      priceAmountMicros: undefined,2277    });2278  });2279 2280  it("treats pending-acknowledgment subscriptions as entitled while still bindable", () => {2281    const snapshot = buildVerifiedSubscriptionSnapshot({2282      platform: "Android",2283      productId: "premium_monthly",2284      purchaseState: HarmonizedPurchaseState.PENDING_ACKNOWLEDGMENT,2285      subscriptionState: "SUBSCRIPTION_STATE_ACTIVE",2286    });2287 2288    expect(snapshot).toMatchObject({2289      productId: "premium_monthly",2290      state: "Active",2291      willRenew: true,2292      clearCancellationReason: true,2293    });2294  });2295 2296  it("stamps Refunded only when Apple says the revocation was a refund", () => {2297    // revocationReason 1 is Apple's app-issue refund; 0 also covers Family2298    // Sharing loss, where asserting money moved back would be false.2299    const refunded = buildVerifiedSubscriptionSnapshot({2300      platform: "IOS",2301      productId: "premium_monthly",2302      purchaseState: HarmonizedPurchaseState.CANCELED,2303      revocationReasonIOS: 1,2304    });2305    expect(refunded).toMatchObject({2306      state: "Revoked",2307      willRenew: false,2308      cancellationReason: "Refunded",2309    });2310 2311    const familySharingLoss = buildVerifiedSubscriptionSnapshot({2312      platform: "IOS",2313      productId: "premium_monthly",2314      purchaseState: HarmonizedPurchaseState.CANCELED,2315      revocationReasonIOS: 0,2316    });2317    expect(familySharingLoss).toMatchObject({2318      state: "Revoked",2319      willRenew: false,2320    });2321    expect(familySharingLoss?.cancellationReason).toBeUndefined();2322 2323    const unknown = buildVerifiedSubscriptionSnapshot({2324      platform: "IOS",2325      productId: "premium_monthly",2326      purchaseState: HarmonizedPurchaseState.CANCELED,2327    });2328    expect(unknown?.cancellationReason).toBeUndefined();2329  });2330 2331  it("preserves access for canceled Google subscriptions until expiry", () => {2332    const snapshot = buildVerifiedSubscriptionSnapshot({2333      platform: "Android",2334      productId: "premium_monthly",2335      purchaseState: HarmonizedPurchaseState.CANCELED,2336      subscriptionState: "SUBSCRIPTION_STATE_CANCELED",2337      expiresAt: 2_000_000_000_000,2338    });2339 2340    expect(snapshot).toMatchObject({2341      productId: "premium_monthly",2342      state: "Active",2343      willRenew: false,2344      expiresAt: 2_000_000_000_000,2345    });2346    expect(snapshot?.cancellationReason).toBeUndefined();2347  });2348 2349  it("maps on-hold Google subscriptions to billing retry", () => {2350    const snapshot = buildVerifiedSubscriptionSnapshot({2351      platform: "Android",2352      productId: "premium_monthly",2353      purchaseState: HarmonizedPurchaseState.PENDING,2354      subscriptionState: "SUBSCRIPTION_STATE_ON_HOLD",2355    });2356 2357    expect(snapshot).toMatchObject({2358      productId: "premium_monthly",2359      state: "InBillingRetry",2360      cancellationReason: "BillingError",2361    });2362  });2363 2364  it("does not infer renewal status when Google omits subscriptionState", () => {2365    const snapshot = buildVerifiedSubscriptionSnapshot({2366      platform: "Android",2367      productId: "premium_monthly",2368      purchaseState: HarmonizedPurchaseState.PENDING_ACKNOWLEDGMENT,2369    });2370 2371    expect(snapshot).toMatchObject({2372      productId: "premium_monthly",2373      state: "Active",2374    });2375    expect(snapshot?.willRenew).toBeUndefined();2376    expect(snapshot?.cancellationReason).toBeUndefined();2377    expect(snapshot?.clearCancellationReason).toBeUndefined();2378  });2379 2380  it("does not create a subscription row for expected-product mismatches", () => {2381    const snapshot = buildVerifiedSubscriptionSnapshot({2382      platform: "Android",2383      productId: "premium_monthly",2384      purchaseState: HarmonizedPurchaseState.INAUTHENTIC,2385      subscriptionState: "SUBSCRIPTION_STATE_ACTIVE",2386    });2387 2388    expect(snapshot).toBeNull();2389  });2390 2391  it("does not create a subscription row when Google omits the product id", () => {2392    const snapshot = buildVerifiedSubscriptionSnapshot({2393      platform: "Android",2394      productId: "unknown",2395      purchaseState: HarmonizedPurchaseState.ENTITLED,2396      subscriptionState: "SUBSCRIPTION_STATE_ACTIVE",2397    });2398 2399    expect(snapshot).toBeNull();2400  });2401 2402  it("bootstraps an Apple subscription without guessing auto-renew status", () => {2403    const snapshot = buildVerifiedSubscriptionSnapshot({2404      platform: "IOS",2405      productId: "com.example.premium",2406      purchaseState: HarmonizedPurchaseState.ENTITLED,2407      expiresAt: 2_000_000_000_000,2408      currency: "USD",2409      priceAmountMicros: 9_990_000,2410    });2411 2412    expect(snapshot).toEqual({2413      productId: "com.example.premium",2414      state: "Active",2415      expiresAt: 2_000_000_000_000,2416      renewsAt: undefined,2417      cancellationReason: undefined,2418      clearCancellationReason: true,2419      currency: "USD",2420      priceAmountMicros: 9_990_000,2421    });2422  });2423});2424 2425describe("mergeVerifiedSubscriptionSnapshot", () => {2426  it("preserves existing timing and price fields when verify omits them", () => {2427    const snapshot = mergeVerifiedSubscriptionSnapshot(2428      {2429        expiresAt: 2_000_000_000_000,2430        renewsAt: 2_000_000_000_000,2431        willRenew: true,2432        cancellationReason: undefined,2433        currency: "USD",2434        priceAmountMicros: 9_990_000,2435      },2436      {2437        productId: "premium_monthly",2438        state: "Active",2439        willRenew: true,2440      },2441    );2442 2443    expect(snapshot).toEqual({2444      productId: "premium_monthly",2445      state: "Active",2446      expiresAt: 2_000_000_000_000,2447      renewsAt: 2_000_000_000_000,2448      willRenew: true,2449      cancellationReason: undefined,2450      currency: "USD",2451      priceAmountMicros: 9_990_000,2452    });2453  });2454 2455  it("clears stale cancellation reason when verified snapshots request it", () => {2456    const existing = {2457      expiresAt: undefined,2458      renewsAt: undefined,2459      willRenew: false,2460      cancellationReason: "UserCanceled" as const,2461      currency: undefined,2462      priceAmountMicros: undefined,2463    };2464    const appleSnapshot = buildVerifiedSubscriptionSnapshot({2465      platform: "IOS",2466      productId: "premium_monthly",2467      purchaseState: HarmonizedPurchaseState.ENTITLED,2468    });2469    const graceSnapshot = buildVerifiedSubscriptionSnapshot({2470      platform: "Android",2471      productId: "premium_monthly",2472      purchaseState: HarmonizedPurchaseState.ENTITLED,2473      subscriptionState: "SUBSCRIPTION_STATE_IN_GRACE_PERIOD",2474    });2475 2476    expect(appleSnapshot?.clearCancellationReason).toBe(true);2477    expect(graceSnapshot?.clearCancellationReason).toBe(true);2478 2479    const appleMerged = mergeVerifiedSubscriptionSnapshot(2480      existing,2481      appleSnapshot!,2482    );2483    const graceMerged = mergeVerifiedSubscriptionSnapshot(2484      existing,2485      graceSnapshot!,2486    );2487 2488    expect(appleMerged.cancellationReason).toBeUndefined();2489    expect(graceMerged.cancellationReason).toBeUndefined();2490    expect(graceMerged.willRenew).toBe(true);2491  });2492 2493  it("preserves cancellation reason when verify cannot prove auto-renew is enabled", () => {2494    const snapshot = mergeVerifiedSubscriptionSnapshot(2495      {2496        expiresAt: undefined,2497        renewsAt: undefined,2498        willRenew: false,2499        cancellationReason: "UserCanceled",2500        currency: undefined,2501        priceAmountMicros: undefined,2502      },2503      {2504        productId: "premium_monthly",2505        state: "Active",2506      },2507    );2508 2509    expect(snapshot.cancellationReason).toBe("UserCanceled");2510    expect(snapshot.willRenew).toBe(false);2511  });2512});2513 2514describe("recordVerifiedSubscriptionHandler", () => {2515  beforeEach(() => {2516    vi.setSystemTime(new Date("2026-01-01T00:00:00.000Z"));2517  });2518 2519  afterEach(() => {2520    vi.useRealTimers();2521  });2522 2523  it("rejects a receipt-verification write once project deletion starts", async () => {2524    const db = new MemDb();2525    await db.patch(PROJECT_ID, { pendingDeletion: true });2526 2527    await expect(2528      recordVerifiedSubscriptionHandler(makeCtx(db), {2529        projectId: PROJECT_ID as never,2530        platform: "Android",2531        purchaseToken: TOKEN,2532        productId: "premium_monthly",2533        purchaseState: HarmonizedPurchaseState.ENTITLED,2534      }),2535    ).rejects.toThrow("Project not found");2536    expect(db.rows("subscriptions")).toEqual([]);2537    expect(db.rows("subscriptionStats")).toEqual([]);2538  });2539 2540  it("creates a bindable subscription row from Google receipt verification", async () => {2541    const db = new MemDb();2542    db.seedProduct({2543      projectId: PROJECT_ID,2544      platform: "Android",2545      productId: "premium_monthly",2546      billingPeriod: "P1M",2547    });2548 2549    const subscriptionId = await recordVerifiedSubscriptionHandler(2550      makeCtx(db),2551      {2552        projectId: PROJECT_ID as never,2553        platform: "Android",2554        purchaseToken: TOKEN,2555        productId: "premium_monthly",2556        purchaseState: HarmonizedPurchaseState.ENTITLED,2557        subscriptionState: "SUBSCRIPTION_STATE_ACTIVE",2558        expiresAt: 1_769_904_000_000,2559        renewsAt: 1_769_904_000_000,2560        currency: "USD",2561        priceAmountMicros: 9_990_000,2562      },2563    );2564 2565    expect(subscriptionId).toBe("subscriptions_2");2566    expect(db.rows("subscriptions")).toMatchObject([2567      {2568        _id: "subscriptions_2",2569        projectId: PROJECT_ID,2570        purchaseToken: TOKEN,2571        productId: "premium_monthly",2572        platform: "Android",2573        state: "Active",2574        willRenew: true,2575        currency: "USD",2576        priceAmountMicros: 9_990_000,2577      },2578    ]);2579    expect(db.rows("subscriptionStats")).toMatchObject([2580      {2581        projectId: PROJECT_ID,2582        currency: "USD",2583        activeSubs: 1,2584        inGracePeriod: 0,2585        inBillingRetry: 0,2586        mrrMicros: 9_990_000,2587      },2588    ]);2589  });2590 2591  it("keeps a verified Google prepaid subscription non-renewing", async () => {2592    const db = new MemDb();2593 2594    await recordVerifiedSubscriptionHandler(makeCtx(db), {2595      projectId: PROJECT_ID as never,2596      platform: "Android",2597      purchaseToken: "prepaid-token",2598      productId: "prepaid_monthly",2599      purchaseState: HarmonizedPurchaseState.ENTITLED,2600      subscriptionState: "SUBSCRIPTION_STATE_ACTIVE",2601      expiresAt: 1_769_904_000_000,2602      willRenew: false,2603    });2604 2605    expect(db.rows("subscriptions")).toMatchObject([2606      {2607        purchaseToken: "prepaid-token",2608        state: "Active",2609        expiresAt: 1_769_904_000_000,2610        willRenew: false,2611        renewsAt: undefined,2612      },2613    ]);2614  });2615 2616  it("marks a notification-less bootstrap row Unknown when an IOS verify turns UNKNOWN", async () => {2617    const db = new MemDb();2618    const input = {2619      projectId: PROJECT_ID as never,2620      platform: "IOS" as const,2621      purchaseToken: "ios-unknown-reason-token",2622      productId: "premium_monthly",2623      purchaseState: HarmonizedPurchaseState.ENTITLED,2624      expiresAt: 1_769_904_000_000,2625    };2626 2627    await recordVerifiedSubscriptionHandler(makeCtx(db), input);2628    expect(db.rows("subscriptions")).toMatchObject([2629      { purchaseToken: "ios-unknown-reason-token", state: "Active" },2630    ]);2631 2632    await recordVerifiedSubscriptionHandler(makeCtx(db), {2633      ...input,2634      purchaseState: HarmonizedPurchaseState.UNKNOWN,2635    });2636    expect(db.rows("subscriptions")).toMatchObject([2637      { purchaseToken: "ios-unknown-reason-token", state: "Unknown" },2638    ]);2639  });2640 2641  it("ignores an older iOS renewal verified after a newer one", async () => {2642    const db = new MemDb();2643    const day = 86_400_000;2644    const now = Date.now();2645    const input = {2646      projectId: PROJECT_ID as Id<"projects">,2647      platform: "IOS" as const,2648      purchaseToken: "ios-original-transaction",2649      productId: "pro_monthly",2650    };2651 2652    await recordVerifiedSubscriptionHandler(makeCtx(db), {2653      ...input,2654      purchaseState: HarmonizedPurchaseState.ENTITLED,2655      expiresAt: now + 20 * day,2656      verifiedPurchaseDate: now - 10 * day,2657    });2658    await recordVerifiedSubscriptionHandler(makeCtx(db), {2659      ...input,2660      purchaseState: HarmonizedPurchaseState.EXPIRED,2661      expiresAt: now - 10 * day,2662      verifiedPurchaseDate: now - 40 * day,2663    });2664 2665    expect(db.rows("subscriptions")).toMatchObject([2666      { state: "Active", expiresAt: now + 20 * day },2667    ]);2668  });2669 2670  it("applies a newer refunded plan and a refund of the stored transaction", async () => {2671    const db = new MemDb();2672    const day = 86_400_000;2673    const now = Date.now();2674    const input = {2675      projectId: PROJECT_ID as Id<"projects">,2676      platform: "IOS" as const,2677      purchaseToken: "ios-original-transaction",2678    };2679    await recordVerifiedSubscriptionHandler(makeCtx(db), {2680      ...input,2681      productId: "basic_yearly",2682      purchaseState: HarmonizedPurchaseState.ENTITLED,2683      expiresAt: now + 300 * day,2684      verifiedPurchaseDate: now - 65 * day,2685    });2686    await recordVerifiedSubscriptionHandler(makeCtx(db), {2687      ...input,2688      productId: "pro_monthly",2689      purchaseState: HarmonizedPurchaseState.EXPIRED,2690      expiresAt: now - day,2691      verifiedPurchaseDate: now - 31 * day,2692    });2693    expect(db.rows("subscriptions")).toMatchObject([2694      { productId: "pro_monthly", state: "Expired" },2695    ]);2696 2697    // Refunding a transaction keeps its purchase date.2698    await recordVerifiedSubscriptionHandler(makeCtx(db), {2699      ...input,2700      productId: "pro_monthly",2701      purchaseState: HarmonizedPurchaseState.CANCELED,2702      expiresAt: now - day,2703      verifiedPurchaseDate: now - 31 * day,2704      revocationReasonIOS: 1,2705    });2706    expect(db.rows("subscriptions")).toMatchObject([2707      {2708        productId: "pro_monthly",2709        state: "Revoked",2710        cancellationReason: "Refunded",2711        verifiedPurchaseDate: now - 31 * day,2712      },2713    ]);2714  });2715 2716  it("keeps a refund when the pre-refund verify of the same transaction commits last", async () => {2717    const db = new MemDb();2718    const day = 86_400_000;2719    const now = Date.now();2720    const input = {2721      projectId: PROJECT_ID as Id<"projects">,2722      platform: "IOS" as const,2723      purchaseToken: "ios-original-transaction",2724      productId: "pro_monthly",2725      expiresAt: now + 20 * day,2726      verifiedPurchaseDate: now - 10 * day,2727    };2728    // Stamped before signing dates were kept: the refund applies by purchase date alone.2729    await recordVerifiedSubscriptionHandler(makeCtx(db), {2730      ...input,2731      purchaseState: HarmonizedPurchaseState.ENTITLED,2732    });2733 2734    // Concurrent verifies fetched before and after the refund; the refund commits first.2735    await recordVerifiedSubscriptionHandler(makeCtx(db), {2736      ...input,2737      purchaseState: HarmonizedPurchaseState.CANCELED,2738      revocationReasonIOS: 1,2739      verifiedSignedDate: now,2740    });2741    await recordVerifiedSubscriptionHandler(makeCtx(db), {2742      ...input,2743      purchaseState: HarmonizedPurchaseState.ENTITLED,2744      verifiedSignedDate: now - 1_000,2745    });2746    // Nor can a same-transaction verify without a signing date.2747    await recordVerifiedSubscriptionHandler(makeCtx(db), {2748      ...input,2749      purchaseState: HarmonizedPurchaseState.ENTITLED,2750    });2751 2752    expect(db.rows("subscriptions")).toMatchObject([2753      {2754        state: "Revoked",2755        cancellationReason: "Refunded",2756        verifiedPurchaseDate: now - 10 * day,2757        verifiedSignedDate: now,2758      },2759    ]);2760  });2761 2762  it("applies the first verify to an iOS row stored without a purchase date and stamps it", async () => {2763    const db = new MemDb();2764    const day = 86_400_000;2765    const now = Date.now();2766    const input = {2767      projectId: PROJECT_ID as Id<"projects">,2768      platform: "IOS" as const,2769      purchaseToken: "ios-original-transaction",2770    };2771    // A row written before deploy carries no verifiedPurchaseDate.2772    await recordVerifiedSubscriptionHandler(makeCtx(db), {2773      ...input,2774      productId: "pro_monthly",2775      purchaseState: HarmonizedPurchaseState.ENTITLED,2776      expiresAt: now + 30 * day,2777    });2778 2779    // An old replay applies once, as before purchase dates were kept...2780    await recordVerifiedSubscriptionHandler(makeCtx(db), {2781      ...input,2782      productId: "basic_yearly",2783      purchaseState: HarmonizedPurchaseState.EXPIRED,2784      expiresAt: now - 200 * day,2785      verifiedPurchaseDate: now - 300 * day,2786    });2787    expect(db.rows("subscriptions")).toMatchObject([2788      {2789        productId: "basic_yearly",2790        state: "Expired",2791        verifiedPurchaseDate: now - 300 * day,2792      },2793    ]);2794 2795    // ...and the next newer verify corrects it.2796    await recordVerifiedSubscriptionHandler(makeCtx(db), {2797      ...input,2798      productId: "pro_monthly",2799      purchaseState: HarmonizedPurchaseState.ENTITLED,2800      expiresAt: now + 30 * day,2801      verifiedPurchaseDate: now,2802    });2803    expect(db.rows("subscriptions")).toMatchObject([2804      { productId: "pro_monthly", state: "Active", verifiedPurchaseDate: now },2805    ]);2806  });2807 2808  it("rejects an older verify that commits after a concurrent one stamped the row", async () => {2809    const db = new MemDb();2810    const day = 86_400_000;2811    const now = Date.now();2812    const input = {2813      projectId: PROJECT_ID as Id<"projects">,2814      platform: "IOS" as const,2815      purchaseToken: "ios-original-transaction",2816      purchaseState: HarmonizedPurchaseState.ENTITLED,2817    };2818    await recordVerifiedSubscriptionHandler(makeCtx(db), {2819      ...input,2820      productId: "basic_yearly",2821      expiresAt: now + 300 * day,2822    });2823 2824    // Both requests read the row undated; the newer one commits first.2825    await recordVerifiedSubscriptionHandler(makeCtx(db), {2826      ...input,2827      productId: "pro_monthly",2828      expiresAt: now + 30 * day,2829      verifiedPurchaseDate: now,2830    });2831    await recordVerifiedSubscriptionHandler(makeCtx(db), {2832      ...input,2833      productId: "plus_monthly",2834      expiresAt: now + 20 * day,2835      verifiedPurchaseDate: now - 10 * day,2836    });2837 2838    expect(db.rows("subscriptions")).toMatchObject([2839      { productId: "pro_monthly", verifiedPurchaseDate: now },2840    ]);2841  });2842 2843  it("applies an immediate iOS upgrade that ends sooner than the plan it replaced", async () => {2844    const db = new MemDb();2845    const day = 86_400_000;2846    const now = Date.now();2847    const input = {2848      projectId: PROJECT_ID as Id<"projects">,2849      platform: "IOS" as const,2850      purchaseToken: "ios-original-transaction",2851      purchaseState: HarmonizedPurchaseState.ENTITLED,2852    };2853 2854    await recordVerifiedSubscriptionHandler(makeCtx(db), {2855      ...input,2856      productId: "basic_yearly",2857      expiresAt: now + 300 * day,2858      verifiedPurchaseDate: now - 65 * day,2859    });2860    await recordVerifiedSubscriptionHandler(makeCtx(db), {2861      ...input,2862      productId: "pro_monthly",2863      expiresAt: now + 30 * day,2864      verifiedPurchaseDate: now,2865    });2866 2867    expect(db.rows("subscriptions")).toMatchObject([2868      { productId: "pro_monthly", expiresAt: now + 30 * day },2869    ]);2870  });2871 2872  it("clears a prior renewal date when verification becomes non-renewing", async () => {2873    const db = new MemDb();2874    const input = {2875      projectId: PROJECT_ID as never,2876      platform: "Android" as const,2877      purchaseToken: "prepaid-transition-token",2878      productId: "premium_monthly",2879      purchaseState: HarmonizedPurchaseState.ENTITLED,2880      subscriptionState: "SUBSCRIPTION_STATE_ACTIVE",2881      expiresAt: 1_769_904_000_000,2882    };2883 2884    await recordVerifiedSubscriptionHandler(makeCtx(db), {2885      ...input,2886      renewsAt: 1_769_904_000_000,2887      willRenew: true,2888    });2889    await recordVerifiedSubscriptionHandler(makeCtx(db), {2890      ...input,2891      willRenew: false,2892    });2893 2894    expect(db.rows("subscriptions")).toMatchObject([2895      {2896        purchaseToken: "prepaid-transition-token",2897        state: "Active",2898        willRenew: false,2899        renewsAt: undefined,2900      },2901    ]);2902    expect(db.rows("subscriptions")).toHaveLength(1);2903  });2904 2905  it("creates an active bindable row for pending-acknowledgment Google subscriptions", async () => {2906    const db = new MemDb();2907    db.seedProduct({2908      projectId: PROJECT_ID,2909      platform: "Android",2910      productId: "premium_monthly",2911      billingPeriod: "P1M",2912    });2913 2914    await recordVerifiedSubscriptionHandler(makeCtx(db), {2915      projectId: PROJECT_ID as never,2916      platform: "Android",2917      purchaseToken: TOKEN,2918      productId: "premium_monthly",2919      purchaseState: HarmonizedPurchaseState.PENDING_ACKNOWLEDGMENT,2920      subscriptionState: "SUBSCRIPTION_STATE_ACTIVE",2921      expiresAt: 1_769_904_000_000,2922      currency: "USD",2923      priceAmountMicros: 9_990_000,2924    });2925 2926    expect(db.rows("subscriptions")).toMatchObject([2927      {2928        purchaseToken: TOKEN,2929        productId: "premium_monthly",2930        platform: "Android",2931        state: "Active",2932        willRenew: true,2933      },2934    ]);2935    expect(db.rows("subscriptionStats")).toMatchObject([2936      {2937        activeSubs: 1,2938        mrrMicros: 9_990_000,2939      },2940    ]);2941  });2942 2943  it("keeps repeated verification idempotent for subscription stats", async () => {2944    const db = new MemDb();2945    db.seedProduct({2946      projectId: PROJECT_ID,2947      platform: "Android",2948      productId: "premium_monthly",2949      billingPeriod: "P1M",2950    });2951    const args = {2952      projectId: PROJECT_ID as never,2953      platform: "Android" as const,2954      purchaseToken: TOKEN,2955      productId: "premium_monthly",2956      purchaseState: HarmonizedPurchaseState.ENTITLED,2957      subscriptionState: "SUBSCRIPTION_STATE_ACTIVE",2958      expiresAt: 1_769_904_000_000,2959      currency: "USD",2960      priceAmountMicros: 9_990_000,2961    };2962 2963    await recordVerifiedSubscriptionHandler(makeCtx(db), args);2964    await recordVerifiedSubscriptionHandler(makeCtx(db), args);2965 2966    expect(db.rows("subscriptions")).toHaveLength(1);2967    expect(db.rows("subscriptionStats")).toMatchObject([2968      {2969        activeSubs: 1,2970        mrrMicros: 9_990_000,2971      },2972    ]);2973  });2974 2975  it("does not let an older verification overwrite webhook-governed state", async () => {2976    const db = new MemDb();2977    const startedId = await seedWebhookEvent(db, {2978      type: "SubscriptionStarted",2979      notificationId: "apple-monthly",2980      occurredAt: 1_000,2981      platform: "IOS",2982      productId: "premium_monthly",2983    });2984    await applySubscriptionEventHandler(makeCtx(db), {2985      projectId: PROJECT_ID as never,2986      eventId: startedId as never,2987    });2988    const renewedId = await seedWebhookEvent(db, {2989      type: "SubscriptionRenewed",2990      notificationId: "apple-yearly",2991      occurredAt: 2_000,2992      platform: "IOS",2993      productId: "premium_yearly",2994    });2995    await db.patch(renewedId, { expiresAt: 1_900_000_000_000 });2996    await applySubscriptionEventHandler(makeCtx(db), {2997      projectId: PROJECT_ID as never,2998      eventId: renewedId as never,2999    });3000 3001    await recordVerifiedSubscriptionHandler(makeCtx(db), {3002      projectId: PROJECT_ID as never,3003      platform: "IOS",3004      purchaseToken: TOKEN,3005      productId: "premium_monthly",3006      purchaseState: HarmonizedPurchaseState.ENTITLED,3007      expiresAt: 1_700_000_000_000,3008    });3009    expect(db.rows("subscriptions")[0]).toMatchObject({3010      productId: "premium_yearly",3011      expiresAt: 1_900_000_000_000,3012      lastEventSourceNotificationId: "apple-yearly",3013    });3014  });3015 3016  it("creates a bindable Apple subscription row from receipt verification", async () => {3017    const db = new MemDb();3018    db.seedProduct({3019      projectId: PROJECT_ID,3020      platform: "IOS",3021      productId: "com.example.premium",3022      billingPeriod: "P1M",3023    });3024 3025    const subscriptionId = await recordVerifiedSubscriptionHandler(3026      makeCtx(db),3027      {3028        projectId: PROJECT_ID as never,3029        platform: "IOS",3030        purchaseToken: "original_transaction_1",3031        productId: "com.example.premium",3032        purchaseState: HarmonizedPurchaseState.ENTITLED,3033        expiresAt: 1_769_904_000_000,3034        currency: "USD",3035        priceAmountMicros: 9_990_000,3036      },3037    );3038 3039    expect(subscriptionId).toBe("subscriptions_2");3040    expect(db.rows("subscriptions")).toMatchObject([3041      {3042        _id: "subscriptions_2",3043        projectId: PROJECT_ID,3044        purchaseToken: "original_transaction_1",3045        productId: "com.example.premium",3046        platform: "IOS",3047        state: "Active",3048        willRenew: undefined,3049      },3050    ]);3051    expect(db.rows("subscriptionStats")).toMatchObject([3052      {3053        projectId: PROJECT_ID,3054        currency: "USD",3055        activeSubs: 1,3056        mrrMicros: 9_990_000,3057      },3058    ]);3059  });3060 3061  it("supports the verify -> bind flow for SDK clients", async () => {3062    const db = new MemDb();3063    db.seedProduct({3064      projectId: PROJECT_ID,3065      platform: "Android",3066      productId: "premium_monthly",3067      billingPeriod: "P1M",3068    });3069 3070    const subscriptionId = await recordVerifiedSubscriptionHandler(3071      makeCtx(db),3072      {3073        projectId: PROJECT_ID as never,3074        platform: "Android",3075        purchaseToken: TOKEN,3076        productId: "premium_monthly",3077        purchaseState: HarmonizedPurchaseState.ENTITLED,3078        subscriptionState: "SUBSCRIPTION_STATE_ACTIVE",3079        expiresAt: 1_769_904_000_000,3080        renewsAt: 1_769_904_000_000,3081        currency: "USD",3082        priceAmountMicros: 9_990_000,3083      },3084    );3085 3086    const boundId = await bindSubscriptionToUserHandler(makeCtx(db), {3087      projectId: PROJECT_ID as never,3088      purchaseToken: TOKEN,3089      userId: "user-1",3090    });3091 3092    expect(boundId).toBe(subscriptionId);3093    expect(db.rows("subscriptions")).toMatchObject([3094      {3095        _id: subscriptionId,3096        projectId: PROJECT_ID,3097        purchaseToken: TOKEN,3098        productId: "premium_monthly",3099        platform: "Android",3100        state: "Active",3101        userId: "user-1",3102        willRenew: true,3103        currency: "USD",3104        priceAmountMicros: 9_990_000,3105      },3106    ]);3107 3108    const eventId = await seedWebhookEvent(db, {3109      type: "SubscriptionStarted",3110      notificationId: "message-after-bind",3111      occurredAt: 2_000,3112    });3113    await applySubscriptionEventHandler(makeCtx(db), {3114      projectId: PROJECT_ID as never,3115      eventId: eventId as never,3116    });3117    expect(db.rows("commerceEvents")).toMatchObject([3118      { eventType: "subscription.started", userId: "user-1" },3119      { eventType: "entitlement.granted", userId: "user-1" },3120    ]);3121  });3122 3123  it("rejects unbounded user ids at the Convex storage boundary", async () => {3124    const db = new MemDb();3125 3126    for (const userId of ["   ", "u".repeat(257)]) {3127      await expect(3128        bindSubscriptionToUserHandler(makeCtx(db), {3129          projectId: PROJECT_ID as never,3130          purchaseToken: TOKEN,3131          userId,3132        }),3133      ).rejects.toThrow("userId must be nonblank and at most 256 characters");3134    }3135    expect(db.rows("subscriptions")).toHaveLength(0);3136    expect(db.rows("commerceEvents")).toHaveLength(0);3137  });3138 3139  it("emits a correlated grant when the webhook arrives before binding", async () => {3140    const db = new MemDb();3141    db.seedProduct({3142      projectId: PROJECT_ID,3143      platform: "Android",3144      productId: "premium_monthly",3145      billingPeriod: "P1M",3146    });3147    const eventId = await seedWebhookEvent(db, {3148      type: "SubscriptionStarted",3149      notificationId: "message-before-bind",3150      occurredAt: 2_000,3151    });3152    await applySubscriptionEventHandler(makeCtx(db), {3153      projectId: PROJECT_ID as never,3154      eventId: eventId as never,3155    });3156 3157    await bindSubscriptionToUserHandler(makeCtx(db), {3158      projectId: PROJECT_ID as never,3159      purchaseToken: TOKEN,3160      userId: "user-1",3161    });3162 3163    expect(db.rows("commerceEvents").at(-1)).toMatchObject({3164      eventType: "entitlement.granted",3165      userId: "user-1",3166    });3167  });3168 3169  it("does not replay an unbound grant that expired before binding", async () => {3170    const db = new MemDb();3171    db.seedProduct({3172      projectId: PROJECT_ID,3173      platform: "Android",3174      productId: "premium_monthly",3175      billingPeriod: "P1M",3176    });3177    const started = await seedWebhookEvent(db, {3178      type: "SubscriptionStarted",3179      notificationId: "unbound-started",3180      occurredAt: 1_000,3181    });3182    await applySubscriptionEventHandler(makeCtx(db), {3183      projectId: PROJECT_ID as never,3184      eventId: started as never,3185    });3186    const expired = await seedWebhookEvent(db, {3187      type: "SubscriptionExpired",3188      notificationId: "unbound-expired",3189      occurredAt: 2_000,3190    });3191    await applySubscriptionEventHandler(makeCtx(db), {3192      projectId: PROJECT_ID as never,3193      eventId: expired as never,3194    });3195 3196    const beforeBind = db.rows("commerceEvents").map((row) => row.eventType);3197    expect(beforeBind).toEqual([3198      "subscription.started",3199      "subscription.expired",3200    ]);3201 3202    await bindSubscriptionToUserHandler(makeCtx(db), {3203      projectId: PROJECT_ID as never,3204      purchaseToken: TOKEN,3205      userId: "user-after-expiry",3206    });3207 3208    expect(db.rows("commerceEvents").map((row) => row.eventType)).toEqual(3209      beforeBind,3210    );3211    expect(db.rows("subscriptions")[0]).toMatchObject({3212      state: "Expired",3213      userId: "user-after-expiry",3214    });3215  });3216 3217  it("uses the compact source snapshot after the webhook row is pruned", async () => {3218    const db = new MemDb();3219    const eventId = await seedWebhookEvent(db, {3220      type: "SubscriptionStarted",3221      notificationId: "old-webhook",3222      occurredAt: 2_000,3223    });3224    await applySubscriptionEventHandler(makeCtx(db), {3225      projectId: PROJECT_ID as never,3226      eventId: eventId as never,3227    });3228    await db.delete(eventId);3229 3230    await bindSubscriptionToUserHandler(makeCtx(db), {3231      projectId: PROJECT_ID as never,3232      purchaseToken: TOKEN,3233      userId: "user-after-retention",3234    });3235    expect(db.rows("commerceEvents").at(-1)).toMatchObject({3236      eventType: "entitlement.granted",3237      productId: "premium_monthly",3238      userId: "user-after-retention",3239      sourceStoreNotificationId: "old-webhook",3240    });3241  });3242});3243 3244describe("user binding authorization", () => {3245  const seedBound = async (db: MemDb, userId?: string) => {3246    db.seedProduct({3247      projectId: PROJECT_ID,3248      platform: "Android",3249      productId: "premium_monthly",3250      billingPeriod: "P1M",3251    });3252    await db.insert("subscriptions", {3253      projectId: PROJECT_ID,3254      platform: "Android",3255      purchaseToken: TOKEN,3256      productId: "premium_monthly",3257      state: "Active",3258      willRenew: true,3259      updatedAt: 0,3260      ...(userId ? { userId } : {}),3261    });3262  };3263 3264  // Token possession is not proof of ownership, and a distinct rejection would3265  // tell any holder of the app-embedded publishable key that a token exists3266  // and belongs to someone.3267  it("reports an already-bound subscription the same as an unknown token", async () => {3268    const owned = new MemDb();3269    await seedBound(owned, "victim");3270    const unknown = new MemDb();3271    await seedBound(unknown, "victim");3272    await unknown.delete(unknown.rows("subscriptions")[0]._id);3273 3274    const attacker = {3275      projectId: PROJECT_ID as never,3276      purchaseToken: TOKEN,3277      userId: "attacker",3278    };3279    await expect(3280      bindSubscriptionToUserHandler(makeCtx(owned), attacker),3281    ).resolves.toBeNull();3282    await expect(3283      bindSubscriptionToUserHandler(makeCtx(unknown), attacker),3284    ).resolves.toBeNull();3285    expect(owned.rows("subscriptions")[0].userId).toBe("victim");3286  });3287 3288  // Erasure unlinked the previous owner. Once the job is gone the record is3289  // unowned, and an operator may associate it again exactly as a purchase can.3290  it("rebinds a record whose erased owner has already been unlinked", async () => {3291    const db = new MemDb();3292    await seedBound(db, undefined);3293    await db.patch(db.rows("subscriptions")[0]._id, { accountErased: true });3294    expect(3295      await rebindSubscriptionToUserHandler(makeCtx(db), {3296        projectId: PROJECT_ID as never,3297        purchaseToken: TOKEN,3298        userId: "real-owner",3299      }),3300    ).not.toBeNull();3301    const row = db.rows("subscriptions")[0];3302    expect(row.userId).toBe("real-owner");3303    expect(row.accountErased).toBeUndefined();3304  });3305 3306  it("still refuses to move a record away from its live owner", async () => {3307    const db = new MemDb();3308    await seedBound(db, "real-owner");3309    expect(3310      await bindSubscriptionToUserHandler(makeCtx(db), {3311        projectId: PROJECT_ID as never,3312        purchaseToken: TOKEN,3313        userId: "someone-else",3314      }),3315    ).toBeNull();3316    expect(db.rows("subscriptions")[0].userId).toBe("real-owner");3317  });3318 3319  it.each(["queued", "running", "completed"])(3320    "refuses rebind into or out of an account with a %s erasure job",3321    async (status) => {3322      for (const erasedUser of ["source-user", "target-user"]) {3323        const db = new MemDb();3324        await seedBound(db, "source-user");3325        const hashKey = "erasure-test-key";3326        await db.patch(PROJECT_ID, { userErasureHashKey: hashKey });3327        await db.insert("subscriptionUserErasureJobs", {3328          projectId: PROJECT_ID,3329          userIdHash: await hmacSha256Hex(hashKey, erasedUser),3330          status,3331        });3332        const before = structuredClone(db.rows("subscriptions"));3333        expect(3334          await rebindSubscriptionToUserHandler(makeCtx(db), {3335            projectId: PROJECT_ID as never,3336            purchaseToken: TOKEN,3337            userId: "target-user",3338          }),3339        ).toBeNull();3340        expect(db.rows("subscriptions")).toEqual(before);3341        expect(db.rows("commerceEvents")).toEqual([]);3342      }3343    },3344  );3345 3346  // A consumer gating access on commerce events must be told the purchase3347  // moved, or the wrong user keeps access and the real one never gets it.3348  it("revokes the old user and grants the new one on a rebind", async () => {3349    const db = new MemDb();3350    db.seedProduct({3351      projectId: PROJECT_ID,3352      platform: "Android",3353      productId: "premium_monthly",3354      billingPeriod: "P1M",3355    });3356    const priorId = await seedWebhookEvent(db, {3357      type: "SubscriptionStarted",3358      notificationId: "rebind-source",3359      occurredAt: 1_000,3360    });3361    await db.insert("subscriptions", {3362      projectId: PROJECT_ID,3363      platform: "Android",3364      purchaseToken: TOKEN,3365      productId: "premium_monthly",3366      state: "Active",3367      willRenew: true,3368      userId: "wrong-user",3369      lastEventId: priorId,3370      lastEventOccurredAt: 1_000,3371      lastEventSourceNotificationId: "rebind-source",3372      lastEventSource: {3373        type: "SubscriptionStarted",3374        environment: "Production",3375        productId: "premium_monthly",3376      },3377      expiresAt: Date.now() + 86_400_000,3378      updatedAt: 0,3379    });3380 3381    await rebindSubscriptionToUserHandler(makeCtx(db), {3382      projectId: PROJECT_ID as never,3383      purchaseToken: TOKEN,3384      userId: "real-owner",3385    });3386 3387    const emitted = db3388      .rows("commerceEvents")3389      .map((r) => [r.eventType, r.userId]);3390    expect(emitted).toEqual([3391      ["entitlement.revoked", "wrong-user"],3392      ["entitlement.granted", "real-owner"],3393    ]);3394  });3395 3396  // A rebind that cannot attribute events leaves the developer backend3397  // believing the old user still owns the purchase. Say so rather than3398  // reporting plain success.3399  it("reports a rebind it could not notify about", async () => {3400    const db = new MemDb();3401    db.seedProduct({3402      projectId: PROJECT_ID,3403      platform: "Android",3404      productId: "premium_monthly",3405      billingPeriod: "P1M",3406    });3407    await db.insert("subscriptions", {3408      projectId: PROJECT_ID,3409      platform: "Android",3410      purchaseToken: TOKEN,3411      productId: "premium_monthly",3412      state: "Active",3413      willRenew: true,3414      userId: "wrong-user",3415      expiresAt: Date.now() + 86_400_000,3416      updatedAt: 0,3417    });3418 3419    const outcome = await rebindSubscriptionToUserHandler(makeCtx(db), {3420      projectId: PROJECT_ID as never,3421      purchaseToken: TOKEN,3422      userId: "real-owner",3423    });3424    expect(outcome?.notified).toBe(false);3425    expect(db.rows("subscriptions")[0].userId).toBe("real-owner");3426    expect(db.rows("commerceEvents")).toEqual([]);3427  });3428 3429  it("lets an operator move a wrong binding", async () => {3430    const db = new MemDb();3431    await seedBound(db, "wrong-user");3432    await expect(3433      rebindSubscriptionToUserHandler(makeCtx(db), {3434        projectId: PROJECT_ID as never,3435        purchaseToken: TOKEN,3436        userId: "real-owner",3437      }),3438    ).resolves.not.toBeNull();3439    expect(db.rows("subscriptions")[0].userId).toBe("real-owner");3440  });3441});3442 3443describe("subscription environment", () => {3444  async function apply(db: MemDb, eventId: string) {3445    return applySubscriptionEventHandler(makeCtx(db), {3446      projectId: PROJECT_ID as never,3447      eventId: eventId as never,3448    });3449  }3450 3451  it("keeps a known environment when Play sends none", async () => {3452    const db = new MemDb();3453    const started = await seedWebhookEvent(db, {3454      type: "SubscriptionStarted",3455      notificationId: "tester-start",3456      occurredAt: 1_000,3457      environment: "Sandbox",3458    });3459    await apply(db, started);3460    // An expiry recorded without a service account.3461    const expired = await seedWebhookEvent(db, {3462      type: "SubscriptionExpired",3463      notificationId: "tester-expiry",3464      occurredAt: 2_000,3465    });3466    await db.patch(expired, { environmentDefaulted: true });3467    await apply(db, expired);3468 3469    expect(db.rows("subscriptions")[0]).toMatchObject({3470      state: "Expired",3471      environment: "Sandbox",3472    });3473  });3474 3475  it("keeps a legacy row's environment from its last store event", async () => {3476    const db = new MemDb();3477    await db.insert("subscriptions", {3478      projectId: PROJECT_ID,3479      purchaseToken: TOKEN,3480      productKind: "subscription",3481      productId: "premium_monthly",3482      platform: "Android",3483      state: "Active",3484      startedAt: 500,3485      updatedAt: 500,3486      lastEventSource: { environment: "Sandbox" },3487    });3488    const expired = await seedWebhookEvent(db, {3489      type: "SubscriptionExpired",3490      notificationId: "legacy-expiry",3491      occurredAt: 2_000,3492    });3493    await db.patch(expired, { environmentDefaulted: true });3494    await apply(db, expired);3495 3496    expect(db.rows("subscriptions")[0]).toMatchObject({3497      state: "Expired",3498      environment: "Sandbox",3499    });3500  });3501});3502 3503describe("subscription analytics facts", () => {3504  async function apply(db: MemDb, eventId: string) {3505    return applySubscriptionEventHandler(makeCtx(db), {3506      projectId: PROJECT_ID as never,3507      eventId: eventId as never,3508    });3509  }3510 3511  function eventRow(db: MemDb, id: string): Row | undefined {3512    return db.rows("webhookEvents").find((row) => row._id === id);3513  }3514 3515  it("reconciles the first paid marker when a late Google link merges a trial", async () => {3516    const db = new MemDb();3517    const trial = await seedWebhookEvent(db, {3518      type: "SubscriptionStarted",3519      notificationId: "linked-trial",3520      occurredAt: 1000,3521    });3522    await db.patch(trial, { freeTrial: true });3523    await apply(db, trial);3524    const paid = await seedWebhookEvent(db, {3525      type: "SubscriptionRenewed",3526      notificationId: "successor-paid",3527      occurredAt: 5000,3528    });3529    await db.patch(paid, { purchaseToken: "successor-token" });3530    await apply(db, paid);3531    expect(eventRow(db, paid)?.paidStart).toBeUndefined();3532 3533    const linked = await seedWebhookEvent(db, {3534      type: "SubscriptionStarted",3535      notificationId: "late-link",3536      occurredAt: 3000,3537    });3538    await db.patch(linked, {3539      purchaseToken: "successor-token",3540      linkedPurchaseToken: TOKEN,3541      replacementCharged: false,3542    });3543    await apply(db, linked);3544 3545    expect(db.rows("subscriptions")).toHaveLength(1);3546    expect(db.rows("subscriptions")[0].analytics).toMatchObject({3547      trialStartedAt: 1000,3548      firstPaidAt: 5000,3549      firstPaidEventId: paid,3550    });3551    expect(eventRow(db, paid)?.paidStart).toBe("trial_conversion");3552    await apply(db, linked);3553    expect(eventRow(db, paid)?.paidStart).toBe("trial_conversion");3554  });3555 3556  it("clears a removed row's paid-start marker when a late Google link merges rows", async () => {3557    const db = new MemDb();3558    const start = await seedWebhookEvent(db, {3559      type: "SubscriptionStarted",3560      notificationId: "predecessor-paid",3561      occurredAt: 1000,3562    });3563    await apply(db, start);3564    const successor = await seedWebhookEvent(db, {3565      type: "SubscriptionStarted",3566      notificationId: "successor-paid-start",3567      occurredAt: 5000,3568    });3569    await db.patch(successor, { purchaseToken: "successor-token" });3570    await apply(db, successor);3571    expect(eventRow(db, successor)?.paidStart).toBe("direct");3572    const linked = await seedWebhookEvent(db, {3573      type: "SubscriptionStarted",3574      notificationId: "late-paid-link",3575      occurredAt: 3000,3576    });3577    await db.patch(linked, {3578      purchaseToken: "successor-token",3579      linkedPurchaseToken: TOKEN,3580      replacementCharged: false,3581    });3582    await apply(db, linked);3583    expect(eventRow(db, start)?.paidStart).toBe("direct");3584    expect(eventRow(db, successor)?.paidStart).toBeUndefined();3585  });3586 3587  it("marks a trial's first paid charge and never counts a replay twice", async () => {3588    const db = new MemDb();3589    const trial = await seedWebhookEvent(db, {3590      type: "SubscriptionStarted",3591      notificationId: "trial-start",3592      occurredAt: 1_000,3593    });3594    await db.patch(trial, { freeTrial: true });3595    const paid = await seedWebhookEvent(db, {3596      type: "SubscriptionRenewed",3597      notificationId: "first-charge",3598      occurredAt: 2_000,3599    });3600 3601    await apply(db, trial);3602    await apply(db, paid);3603    await apply(db, paid);3604 3605    expect(db.rows("subscriptions")[0].analytics).toMatchObject({3606      trialStartedAt: 1_000,3607      firstPaidAt: 2_000,3608      firstPaidEventId: paid,3609      firstPaidEventType: "SubscriptionRenewed",3610      currency: "USD",3611      paidMicros: 9_990_000,3612      refundedMicros: 0,3613    });3614    expect(eventRow(db, paid)?.paidStart).toBe("trial_conversion");3615    expect(eventRow(db, trial)?.paidStart).toBeUndefined();3616  });3617 3618  it("converges when the trial and charges arrive out of order", async () => {3619    const db = new MemDb();3620    const trial = await seedWebhookEvent(db, {3621      type: "SubscriptionStarted",3622      notificationId: "trial-start",3623      occurredAt: 1_000,3624    });3625    await db.patch(trial, { freeTrial: true });3626    const firstCharge = await seedWebhookEvent(db, {3627      type: "SubscriptionRenewed",3628      notificationId: "first-charge",3629      occurredAt: 2_000,3630    });3631    const secondCharge = await seedWebhookEvent(db, {3632      type: "SubscriptionRenewed",3633      notificationId: "second-charge",3634      occurredAt: 3_000,3635    });3636 3637    await apply(db, secondCharge);3638    expect(eventRow(db, secondCharge)?.paidStart).toBeUndefined();3639    await apply(db, firstCharge);3640    await apply(db, trial);3641 3642    expect(db.rows("subscriptions")[0].analytics).toMatchObject({3643      trialStartedAt: 1_000,3644      firstPaidAt: 2_000,3645      firstPaidEventId: firstCharge,3646      paidMicros: 2 * 9_990_000,3647    });3648    expect(eventRow(db, firstCharge)?.paidStart).toBe("trial_conversion");3649    expect(eventRow(db, secondCharge)?.paidStart).toBeUndefined();3650    // State still follows the newest store event.3651    expect(db.rows("subscriptions")[0].lastEventId).toBe(secondCharge);3652  });3653 3654  it("marks a paid start without a trial as direct and moves the marker to an earlier charge", async () => {3655    const db = new MemDb();3656    const renewal = await seedWebhookEvent(db, {3657      type: "SubscriptionRenewed",3658      notificationId: "renewal",3659      occurredAt: 5_000,3660    });3661    const start = await seedWebhookEvent(db, {3662      type: "SubscriptionStarted",3663      notificationId: "start",3664      occurredAt: 1_000,3665    });3666 3667    await apply(db, renewal);3668    await apply(db, start);3669 3670    expect(eventRow(db, start)?.paidStart).toBe("direct");3671    expect(eventRow(db, renewal)?.paidStart).toBeUndefined();3672  });3673 3674  it("marks a resubscribe a paid start only after a trial IAPKit saw lapse", async () => {3675    async function seedResubscribe(db: MemDb): Promise<string> {3676      const id = await seedWebhookEvent(db, {3677        type: "SubscriptionStarted",3678        notificationId: "resubscribe",3679        occurredAt: 5_000,3680        platform: "IOS",3681      });3682      await db.patch(id, { resubscribe: true });3683      return id;3684    }3685 3686    // Paid before IAPKit saw it: a reactivation with no known start, and the3687    // same lifecycle transition as a first purchase.3688    const returning = new MemDb();3689    const resubscribe = await seedResubscribe(returning);3690    await expect(apply(returning, resubscribe)).resolves.toMatchObject({3691      transition: "Started",3692      active: true,3693    });3694    expect(returning.rows("subscriptions")[0].analytics).toMatchObject({3695      firstPaidAt: 5_000,3696      firstPaidResubscribe: true,3697    });3698    expect(eventRow(returning, resubscribe)?.paidStart).toBeUndefined();3699 3700    // A trial that lapsed before it, arriving late: the resubscribe is the3701    // subscription's first paid charge.3702    const db = new MemDb();3703    const paid = await seedResubscribe(db);3704    const trial = await seedWebhookEvent(db, {3705      type: "SubscriptionStarted",3706      notificationId: "trial-start",3707      occurredAt: 1_000,3708      platform: "IOS",3709    });3710    await db.patch(trial, { freeTrial: true });3711    await apply(db, paid);3712    await apply(db, trial);3713    expect(eventRow(db, paid)?.paidStart).toBe("direct");3714  });3715 3716  it("records the environment and keeps sandbox rows out of live stats", async () => {3717    const db = new MemDb();3718    db.seedProduct({3719      projectId: PROJECT_ID,3720      platform: "Android",3721      productId: "premium_monthly",3722      billingPeriod: "P1M",3723    });3724    const sandbox = await seedWebhookEvent(db, {3725      type: "SubscriptionStarted",3726      notificationId: "sandbox-start",3727      occurredAt: 1_000,3728      environment: "Sandbox",3729    });3730    await apply(db, sandbox);3731    expect(db.rows("subscriptions")[0].environment).toBe("Sandbox");3732    expect(db.rows("subscriptionStats")).toMatchObject([3733      { activeSubs: 0, inGracePeriod: 0, inBillingRetry: 0, mrrMicros: 0 },3734    ]);3735 3736    await recordVerifiedSubscriptionHandler(makeCtx(db), {3737      projectId: PROJECT_ID as never,3738      platform: "Android",3739      purchaseToken: "verified_sandbox_token",3740      productId: "premium_monthly",3741      purchaseState: "ENTITLED",3742      subscriptionState: "SUBSCRIPTION_STATE_ACTIVE",3743      currency: "USD",3744      priceAmountMicros: 9_990_000,3745      environment: "Sandbox",3746    });3747    expect(3748      db3749        .rows("subscriptions")3750        .find((row) => row.purchaseToken === "verified_sandbox_token")3751        ?.environment,3752    ).toBe("Sandbox");3753    expect(db.rows("subscriptionStats")).toMatchObject([3754      { activeSubs: 0, inGracePeriod: 0, inBillingRetry: 0, mrrMicros: 0 },3755    ]);3756  });3757 3758  it("never lets an analytics failure block the lifecycle transition", async () => {3759    const db = new MemDb();3760    const patch = db.patch.bind(db);3761    db.patch = async (id, fields) => {3762      if ("analytics" in fields) throw new Error("analytics write failed");3763      await patch(id, fields);3764    };3765    const errors = vi.spyOn(console, "error").mockImplementation(() => {});3766    try {3767      const start = await seedWebhookEvent(db, {3768        type: "SubscriptionStarted",3769        notificationId: "start",3770        occurredAt: 1_000,3771      });3772 3773      await expect(apply(db, start)).resolves.toMatchObject({3774        transition: "Started",3775        active: true,3776      });3777      expect(db.rows("subscriptions")).toMatchObject([3778        { state: "Active", lastEventId: start },3779      ]);3780      expect(db.rows("subscriptions")[0].analytics).toBeUndefined();3781      expect(errors).toHaveBeenCalledWith("[analyticsFacts] skipped", "Error");3782    } finally {3783      errors.mockRestore();3784    }3785  });3786});3787 3788describe("observed subscription access history", () => {3789  const at = (iso: string): number => Date.parse(iso);3790  const janEnd = at("2026-01-31T23:59:59.999Z");3791  const febEnd = at("2026-02-28T23:59:59.999Z");3792  const asOf = at("2026-03-15T12:00:00Z");3793  beforeEach(() => vi.useFakeTimers({ toFake: ["Date"] }));3794  afterEach(() => vi.useRealTimers());3795  function current(db: MemDb): Doc<"subscriptions"> {3796    return db.rows("subscriptions")[0] as Doc<"subscriptions">;3797  }3798  async function apply(3799    db: MemDb,3800    fields: Partial<Doc<"webhookEvents">> &3801      Pick<Doc<"webhookEvents">, "type" | "occurredAt">,3802  ): Promise<void> {3803    const id = await db.insert("webhookEvents", {3804      projectId: PROJECT_ID,3805      purchaseToken: TOKEN,3806      productKind: "subscription",3807      productId: "premium_monthly",3808      platform: "IOS",3809      store: "apple",3810      source: "AppleAppStoreServerNotificationsV2",3811      environment: "Production",3812      sourceNotificationId: `history-${db.rows("webhookEvents").length}`,3813      currency: "USD",3814      priceAmountMicros: 9_990_000,3815      receivedAt: Date.now(),3816      ...fields,3817    });3818    await applySubscriptionEventHandler(makeCtx(db), {3819      projectId: PROJECT_ID as Id<"projects">,3820      eventId: id as Id<"webhookEvents">,3821    });3822  }3823  function january(db: MemDb): CohortBucket | undefined {3824    const buckets = new Map<string, CohortBucket>();3825    addSubscriptionToCohorts(buckets, current(db), asOf);3826    return buckets.get("Production|2026-01|USD");3827  }3828  it.each([false, true])(3829    "keeps the February lapse after resubscribing (late expiry: %s)",3830    async (late) => {3831      const db = new MemDb();3832      const start = {3833        type: "SubscriptionStarted",3834        occurredAt: at("2026-01-01"),3835        purchaseDate: at("2026-01-01"),3836        expiresAt: at("2026-02-01"),3837      } as const;3838      const expiry = {3839        type: "SubscriptionExpired",3840        occurredAt: at("2026-02-01"),3841        expiresAt: at("2026-02-01"),3842      } as const;3843      const returned = {3844        type: "SubscriptionStarted",3845        resubscribe: true,3846        occurredAt: at("2026-03-01"),3847        purchaseDate: at("2026-03-01"),3848        expiresAt: at("2026-04-01"),3849      } as const;3850      vi.setSystemTime(late ? asOf : start.occurredAt);3851      await apply(db, start);3852      if (!late) {3853        vi.setSystemTime(expiry.occurredAt);3854        await apply(db, expiry);3855      }3856      vi.setSystemTime(asOf);3857      await apply(db, returned);3858      if (late) await apply(db, expiry);3859      expect(january(db)).toMatchObject({3860        members: 1,3861        retained: [1, 0, 1],3862        retentionObserved: [1, 1, 1],3863      });3864      expect(current(db).state).toBe("Active");3865    },3866  );3867  it("keeps the refund gap through cancellation and reversal", async () => {3868    const db = new MemDb();3869    vi.setSystemTime(at("2026-01-01"));3870    await apply(db, {3871      type: "SubscriptionStarted",3872      occurredAt: Date.now(),3873      purchaseDate: Date.now(),3874      expiresAt: at("2026-06-01"),3875    });3876    vi.setSystemTime(at("2026-02-01"));3877    await apply(db, {3878      type: "PurchaseRefunded",3879      occurredAt: Date.now(),3880      revocationDate: Date.now(),3881      expiresAt: at("2026-06-01"),3882      cancellationReason: "Refunded",3883    });3884    vi.setSystemTime(at("2026-02-15"));3885    await apply(db, {3886      type: "SubscriptionCanceled",3887      occurredAt: Date.now(),3888      purchaseDate: at("2026-01-01"),3889      expiresAt: at("2026-06-01"),3890      willRenew: false,3891    });3892    vi.setSystemTime(at("2026-03-01"));3893    await apply(db, {3894      type: "SubscriptionStarted",3895      occurredAt: Date.now(),3896      purchaseDate: at("2026-01-01"),3897      expiresAt: at("2026-06-01"),3898      refundReversal: true,3899    });3900    expect(january(db)).toMatchObject({3901      retained: [1, 0, 1],3902      retentionObserved: [1, 1, 1],3903    });3904    expect(current(db).analytics).toMatchObject({3905      paidMicros: 9_990_000,3906      refundedMicros: 0,3907    });3908  });3909  it("uses Apple purchaseDate without changing notification ordering", async () => {3910    const db = new MemDb();3911    vi.setSystemTime(at("2026-02-01T01:00:00Z"));3912    const normalized = normalizeAppleAsn({3913      payload: {3914        notificationType: "SUBSCRIBED",3915        subtype: "INITIAL_BUY",3916        notificationUUID: "late-january",3917        signedDate: Date.now(),3918        data: { environment: "Production" },3919      },3920      transaction: {3921        originalTransactionId: TOKEN,3922        transactionId: "charge",3923        type: "Auto-Renewable Subscription",3924        productId: "premium_monthly",3925        purchaseDate: at("2026-01-31T12:00:00Z"),3926        expiresDate: at("2026-03-01"),3927        currency: "USD",3928        price: 9990,3929      },3930    });3931    await apply(db, normalized);3932    expect(current(db).lastEventOccurredAt).toBe(Date.now());3933    expect(current(db).analytics?.firstPaidAt).toBe(at("2026-01-31T12:00:00Z"));3934    expect(january(db)).toMatchObject({ members: 1, retained: [1, 1, 0] });3935  });3936  it("keeps successor authority across two links and a late predecessor expiry", async () => {3937    const db = new MemDb();3938    const google = {3939      platform: "Android",3940      store: "google",3941      source: "GooglePlayRealTimeDeveloperNotifications",3942    } as const;3943    vi.setSystemTime(at("2026-01-01"));3944    await apply(db, {3945      ...google,3946      type: "SubscriptionStarted",3947      occurredAt: Date.now(),3948      expiresAt: at("2026-05-01"),3949    });3950    vi.setSystemTime(at("2026-03-01"));3951    await apply(db, {3952      ...google,3953      type: "SubscriptionStarted",3954      purchaseToken: "second",3955      linkedPurchaseToken: TOKEN,3956      occurredAt: Date.now(),3957      expiresAt: at("2026-06-01"),3958    });3959    vi.setSystemTime(at("2026-04-01"));3960    await apply(db, {3961      ...google,3962      type: "SubscriptionStarted",3963      purchaseToken: "third",3964      linkedPurchaseToken: "second",3965      occurredAt: Date.now(),3966      expiresAt: at("2026-07-01"),3967    });3968    vi.setSystemTime(at("2026-06-01"));3969    await apply(db, {3970      ...google,3971      type: "SubscriptionExpired",3972      purchaseToken: TOKEN,3973      occurredAt: at("2026-05-01"),3974      expiresAt: at("2026-05-01"),3975    });3976    const history = current(db).analytics?.accessHistory;3977    expect(accessAt(history, janEnd)).toBe(true);3978    expect(accessAt(history, febEnd)).toBe(true);3979    expect(accessAt(history, at("2026-05-31T23:59:59.999Z"))).toBe(true);3980    expect(accessAt(history, Date.now())).toBe(true);3981    expect(current(db).purchaseToken).toBe("third");3982  });3983  it("observes accepted verification without inventing old history or a new charge", async () => {3984    const db = new MemDb();3985    vi.setSystemTime(asOf);3986    const args = {3987      projectId: PROJECT_ID as Id<"projects">,3988      platform: "Android" as const,3989      purchaseToken: TOKEN,3990      existing: null,3991      next: {3992        state: "Active" as const,3993        productId: "premium_monthly",3994        expiresAt: at("2026-06-01"),3995      },3996      now: Date.now(),3997      verifiedEvidenceAt: asOf - 1000,3998    };3999    await persistSubscriptionSnapshot(makeCtx(db), args);4000    const first = current(db);4001    expect(accessAt(first.analytics?.accessHistory, febEnd)).toBeNull();4002    expect(accessAt(first.analytics?.accessHistory, asOf)).toBe(true);4003    expect(first.analytics).toMatchObject({ paidMicros: 0, refundedMicros: 0 });4004    expect(first.analytics?.firstPaidAt).toBeUndefined();4005    vi.setSystemTime(asOf + 86_400_000);4006    await persistSubscriptionSnapshot(makeCtx(db), {4007      ...args,4008      existing: first,4009      now: Date.now(),4010    });4011    expect(current(db).analytics?.accessHistory?.latest?.effectiveAt).toBe(4012      asOf - 1000,4013    );4014  });4015  it("orders delayed Play commits by the store fetch time", async () => {4016    const db = new MemDb();4017    vi.setSystemTime(at("2026-01-03"));4018    const args = {4019      projectId: PROJECT_ID as Id<"projects">,4020      platform: "Android" as const,4021      purchaseToken: TOKEN,4022      existing: null,4023      next: {4024        state: "Active" as const,4025        productId: "premium_monthly",4026        expiresAt: at("2026-06-01"),4027      },4028      now: Date.now(),4029      verifiedEvidenceAt: at("2026-01-01"),4030    };4031    await persistSubscriptionSnapshot(makeCtx(db), args);4032    vi.setSystemTime(at("2026-01-04"));4033    await persistSubscriptionSnapshot(makeCtx(db), {4034      ...args,4035      existing: current(db),4036      next: { ...args.next, state: "Expired" },4037      now: Date.now(),4038      verifiedEvidenceAt: at("2026-01-02"),4039    });4040    expect(current(db).state).toBe("Expired");4041    expect(accessAt(current(db).analytics?.accessHistory, Date.now())).toBe(4042      false,4043    );4044  });4045  it("does not turn an old Apple receipt replay into a new grant", async () => {4046    const db = new MemDb();4047    vi.setSystemTime(at("2026-01-20"));4048    await persistSubscriptionSnapshot(makeCtx(db), {4049      projectId: PROJECT_ID as Id<"projects">,4050      platform: "IOS",4051      purchaseToken: TOKEN,4052      existing: null,4053      next: {4054        state: "Active",4055        productId: "premium_monthly",4056        expiresAt: at("2026-06-01"),4057      },4058      now: Date.now(),4059      verifiedEvidenceAt: at("2026-01-01"),4060      verifiedPurchaseDate: at("2026-01-01"),4061      verifiedSignedDate: at("2026-01-01"),4062    });4063    vi.setSystemTime(at("2026-01-21"));4064    await apply(db, {4065      type: "PurchaseRefunded",4066      occurredAt: at("2026-01-15"),4067      revocationDate: at("2026-01-15"),4068      cancellationReason: "Refunded",4069    });4070    expect(current(db).state).toBe("Refunded");4071    expect(accessAt(current(db).analytics?.accessHistory, janEnd)).toBe(false);4072    expect(4073      accessAt(4074        current(db).analytics?.accessHistory,4075        at("2026-01-14T23:59:59.999Z"),4076      ),4077    ).toBe(true);4078  });4079  it("keeps unknown verified state unverified, not confirmed inactive", async () => {4080    const db = new MemDb();4081    vi.setSystemTime(asOf);4082    await persistSubscriptionSnapshot(makeCtx(db), {4083      projectId: PROJECT_ID as Id<"projects">,4084      platform: "Android",4085      purchaseToken: TOKEN,4086      existing: null,4087      next: { state: "Unknown", productId: "premium_monthly" },4088      now: Date.now(),4089      verifiedEvidenceAt: Date.now(),4090    });4091    expect(accessAt(current(db).analytics?.accessHistory, asOf)).toBeNull();4092  });4093  it("records Apple's immediate upgrade without applying a scheduled preference", async () => {4094    for (const immediate of [false, true]) {4095      const db = new MemDb();4096      vi.setSystemTime(at("2026-01-01"));4097      await apply(db, {4098        type: "SubscriptionStarted",4099        occurredAt: Date.now(),4100        purchaseDate: Date.now(),4101        expiresAt: at("2026-02-01"),4102      });4103      vi.setSystemTime(at("2026-01-15"));4104      await apply(db, {4105        type: "SubscriptionProductChanged",4106        productId: "premium_yearly",4107        occurredAt: Date.now(),4108        purchaseDate: Date.now(),4109        expiresAt: at("2027-01-15"),4110        effectiveImmediately: immediate,4111      });4112      expect(accessAt(current(db).analytics?.accessHistory, febEnd)).toBe(4113        immediate,4114      );4115    }4116  });4117  it("does not backdate the current Play status into an old notification", async () => {4118    const db = new MemDb();4119    vi.setSystemTime(asOf);4120    await apply(db, {4121      type: "SubscriptionStarted",4122      platform: "Android",4123      store: "google",4124      source: "GooglePlayRealTimeDeveloperNotifications",4125      occurredAt: at("2026-01-01"),4126      snapshotObservedAt: asOf,4127      subscriptionState: "Active",4128      expiresAt: at("2026-04-01"),4129    });4130    expect(january(db)).toMatchObject({4131      members: 1,4132      retained: [0, 0, 1],4133      retentionObserved: [0, 0, 1],4134    });4135  });4136  it("keeps the paid period of an Apple receipt first verified after expiry", async () => {4137    const db = new MemDb();4138    const purchaseDate = at("2026-09-01");4139    const expiresAt = at("2026-10-01");4140    vi.setSystemTime(at("2026-10-05"));4141    await recordVerifiedSubscriptionHandler(makeCtx(db), {4142      projectId: PROJECT_ID as Id<"projects">,4143      platform: "IOS",4144      purchaseToken: TOKEN,4145      productId: "premium_monthly",4146      purchaseState: HarmonizedPurchaseState.EXPIRED,4147      expiresAt,4148      verifiedPurchaseDate: purchaseDate,4149      verifiedSignedDate: Date.now(),4150      evidenceAt: purchaseDate,4151    });4152    const row = current(db);4153    const history = row.analytics?.accessHistory;4154    expect(row.state).toBe("Expired");4155    expect(accessAt(history, at("2026-08-31T23:59:59.999Z"))).toBeNull();4156    expect(accessAt(history, at("2026-09-30T23:59:59.999Z"))).toBe(true);4157    expect(accessAt(history, expiresAt)).toBe(false);4158    expect(accessAt(history, Date.now())).toBe(false);4159    expect(row.analytics).toMatchObject({ paidMicros: 0, refundedMicros: 0 });4160    expect(row.analytics?.firstPaidAt).toBeUndefined();4161  });4162  it("keeps natural Apple expiry dated at expiry when a receipt is reverified", async () => {4163    const db = new MemDb();4164    const purchaseDate = at("2026-09-01");4165    const args = {4166      projectId: PROJECT_ID as Id<"projects">,4167      platform: "IOS" as const,4168      purchaseToken: TOKEN,4169      productId: "premium_monthly",4170      expiresAt: at("2026-10-01"),4171      verifiedPurchaseDate: purchaseDate,4172      evidenceAt: purchaseDate,4173    };4174    vi.setSystemTime(at("2026-09-10"));4175    await recordVerifiedSubscriptionHandler(makeCtx(db), {4176      ...args,4177      purchaseState: HarmonizedPurchaseState.ENTITLED,4178      verifiedSignedDate: Date.now(),4179    });4180    vi.setSystemTime(at("2026-10-05"));4181    await recordVerifiedSubscriptionHandler(makeCtx(db), {4182      ...args,4183      purchaseState: HarmonizedPurchaseState.EXPIRED,4184      verifiedSignedDate: Date.now(),4185    });4186    const history = current(db).analytics?.accessHistory;4187    expect(current(db).state).toBe("Expired");4188    expect(accessAt(history, at("2026-09-30T23:59:59.999Z"))).toBe(true);4189    expect(accessAt(history, at("2026-10-04T23:59:59.999Z"))).toBe(false);4190    expect(accessAt(history, Date.now())).toBe(false);4191    expect(history?.latest?.effectiveAt).toBe(purchaseDate);4192  });4193  it("keeps a lapse before a later Apple period first verified after expiry", async () => {4194    const db = new MemDb();4195    const args = {4196      projectId: PROJECT_ID as Id<"projects">,4197      platform: "IOS" as const,4198      purchaseToken: TOKEN,4199      productId: "premium_monthly",4200      purchaseState: HarmonizedPurchaseState.EXPIRED,4201    };4202    vi.setSystemTime(at("2026-03-05"));4203    await recordVerifiedSubscriptionHandler(makeCtx(db), {4204      ...args,4205      expiresAt: at("2026-02-01"),4206      verifiedPurchaseDate: at("2026-01-01"),4207      verifiedSignedDate: Date.now(),4208      evidenceAt: at("2026-01-01"),4209    });4210    vi.setSystemTime(at("2026-04-05"));4211    await recordVerifiedSubscriptionHandler(makeCtx(db), {4212      ...args,4213      expiresAt: at("2026-04-01"),4214      verifiedPurchaseDate: at("2026-03-01"),4215      verifiedSignedDate: Date.now(),4216      evidenceAt: at("2026-03-01"),4217    });4218    const history = current(db).analytics?.accessHistory;4219    expect(accessAt(history, janEnd)).toBe(true);4220    expect(accessAt(history, febEnd)).toBe(false);4221    expect(accessAt(history, at("2026-03-31T23:59:59.999Z"))).toBe(true);4222    expect(accessAt(history, Date.now())).toBe(false);4223    expect(current(db).analytics?.paidMicros).toBe(0);4224  });4225  it("keeps a re-signed change to the same Apple period's expiry undated", async () => {4226    const db = new MemDb();4227    const purchaseDate = at("2026-01-01");4228    const args = {4229      projectId: PROJECT_ID as Id<"projects">,4230      platform: "IOS" as const,4231      purchaseToken: TOKEN,4232      productId: "premium_monthly",4233      verifiedPurchaseDate: purchaseDate,4234      evidenceAt: purchaseDate,4235    };4236    vi.setSystemTime(at("2026-01-10"));4237    await recordVerifiedSubscriptionHandler(makeCtx(db), {4238      ...args,4239      purchaseState: HarmonizedPurchaseState.ENTITLED,4240      expiresAt: at("2026-06-01"),4241      verifiedSignedDate: Date.now(),4242    });4243    vi.setSystemTime(at("2026-01-20"));4244    await recordVerifiedSubscriptionHandler(makeCtx(db), {4245      ...args,4246      purchaseState: HarmonizedPurchaseState.EXPIRED,4247      expiresAt: at("2026-01-15"),4248      verifiedSignedDate: Date.now(),4249    });4250    expect(4251      accessAt(current(db).analytics?.accessHistory, Date.now()),4252    ).toBeNull();4253    vi.setSystemTime(at("2026-01-21"));4254    await recordVerifiedSubscriptionHandler(makeCtx(db), {4255      ...args,4256      purchaseState: HarmonizedPurchaseState.EXPIRED,4257      expiresAt: at("2026-01-15"),4258      verifiedSignedDate: Date.now(),4259    });4260    expect(4261      accessAt(current(db).analytics?.accessHistory, Date.now()),4262    ).toBeNull();4263    expect(4264      accessAt(4265        current(db).analytics?.accessHistory,4266        at("2026-01-14T23:59:59.999Z"),4267      ),4268    ).toBe(true);4269  });4270  it.each([HarmonizedPurchaseState.ENTITLED, HarmonizedPurchaseState.EXPIRED])(4271    "keeps undated Apple %s history unknown",4272    async (purchaseState) => {4273      const db = new MemDb();4274      vi.setSystemTime(at("2026-10-05"));4275      await recordVerifiedSubscriptionHandler(makeCtx(db), {4276        projectId: PROJECT_ID as Id<"projects">,4277        platform: "IOS",4278        purchaseToken: TOKEN,4279        productId: "premium_monthly",4280        purchaseState,4281        expiresAt: at("2026-10-01"),4282        verifiedSignedDate: Date.now(),4283      });4284      const history = current(db).analytics?.accessHistory;4285      expect(accessAt(history, at("2026-09-30T23:59:59.999Z"))).toBeNull();4286      expect(accessAt(history, Date.now())).toBeNull();4287    },4288  );4289  it("keeps an undated receipt-only refund reversal unknown at signing time", async () => {4290    const db = new MemDb();4291    vi.setSystemTime(at("2026-01-16"));4292    const args = {4293      projectId: PROJECT_ID as Id<"projects">,4294      platform: "IOS" as const,4295      purchaseToken: TOKEN,4296      productId: "premium_monthly",4297      purchaseState: HarmonizedPurchaseState.CANCELED,4298      expiresAt: at("2026-06-01"),4299      verifiedPurchaseDate: at("2026-01-01"),4300      verifiedSignedDate: at("2026-01-16"),4301      evidenceAt: at("2026-01-15"),4302    };4303    await recordVerifiedSubscriptionHandler(makeCtx(db), args);4304    expect(current(db).state).toBe("Revoked");4305    expect(accessAt(current(db).analytics?.accessHistory, Date.now())).toBe(4306      false,4307    );4308    vi.setSystemTime(at("2026-01-20"));4309    await recordVerifiedSubscriptionHandler(makeCtx(db), {4310      ...args,4311      purchaseState: HarmonizedPurchaseState.ENTITLED,4312      verifiedSignedDate: Date.now(),4313      evidenceAt: at("2026-01-01"),4314    });4315    expect(current(db).state).toBe("Active");4316    expect(4317      accessAt(current(db).analytics?.accessHistory, Date.now()),4318    ).toBeNull();4319    expect(4320      accessAt(4321        current(db).analytics?.accessHistory,4322        at("2026-01-16T23:59:59.999Z"),4323      ),4324    ).toBe(false);4325    expect(current(db).analytics?.paidMicros).toBe(0);4326  });4327});4328