← Provider replacement walkthrough

IAPKit · convex/subscriptions/internal.test.ts

Source snapshot · 2026-09-09

SHA-256 5d30a9e0d775f11d0c293ea850ab12b6809522f3cac69db3d34ea06c0ed8b11d
1import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";2 3import { HarmonizedPurchaseState } from "../purchases/purchaseState";4import { hmacSha256Hex } from "../utils/sha256";5import {6  applySubscriptionEventHandler,7  bindSubscriptionToUserHandler,8  rebindSubscriptionToUserHandler,9  drainSubscriptionUserErasurePage,10  buildVerifiedSubscriptionSnapshot,11  getCurrentProductIdByTokenHandler,12  getSourceProductIdByTokenHandler,13  mergeVerifiedSubscriptionSnapshot,14  recordVerifiedSubscriptionHandler,15} from "./internal";16 17type Row = Record<string, unknown> & { _id: string; _creationTime: number };18 19class MemQuery {20  constructor(private rows: Row[]) {}21 22  withIndex(_name: string, cb?: (q: IndexBuilder) => IndexBuilder): MemQuery {23    if (!cb) return this;24    const builder = new IndexBuilder();25    cb(builder);26    return new MemQuery(27      this.rows.filter((row) =>28        builder.predicates.every((predicate) => predicate(row)),29      ),30    );31  }32 33  async unique(): Promise<Row | null> {34    if (this.rows.length > 1) {35      throw new Error("unique() called on a query that returned > 1 row");36    }37    return this.rows[0] ?? null;38  }39 40  async collect(): Promise<Row[]> {41    return [...this.rows];42  }43 44  async take(count: number): Promise<Row[]> {45    return this.rows.slice(0, count);46  }47}48 49class IndexBuilder {50  predicates: Array<(row: Row) => boolean> = [];51 52  eq(field: string, value: unknown): IndexBuilder {53    this.predicates.push((row) => row[field] === value);54    return this;55  }56}57 58class MemDb {59  tables = new Map<string, Map<string, Row>>();60  private counter = 0;61 62  constructor() {63    this.table("organizations").set("organizations_seed_1", {64      _id: "organizations_seed_1",65      _creationTime: Date.now(),66    });67    this.table("projects").set("projects_seed_1", {68      _id: "projects_seed_1",69      _creationTime: Date.now(),70      organizationId: "organizations_seed_1",71    });72  }73 74  private table(name: string): Map<string, Row> {75    let table = this.tables.get(name);76    if (!table) {77      table = new Map();78      this.tables.set(name, table);79    }80    return table;81  }82 83  query(tableName: string): MemQuery {84    return new MemQuery([...this.table(tableName).values()]);85  }86 87  async insert(88    tableName: string,89    doc: Record<string, unknown>,90  ): Promise<string> {91    const id = `${tableName}_${++this.counter}`;92    this.table(tableName).set(id, {93      ...doc,94      _id: id,95      _creationTime: Date.now() + this.counter / 1_000,96    });97    return id;98  }99 100  async get(id: string): Promise<Row | null> {101    for (const table of this.tables.values()) {102      const row = table.get(id);103      if (row) return row;104    }105    return null;106  }107 108  async patch(id: string, patch: Record<string, unknown>): Promise<void> {109    for (const table of this.tables.values()) {110      const row = table.get(id);111      if (row) {112        Object.assign(row, patch);113        return;114      }115    }116    throw new Error(`patch: no doc with id ${id}`);117  }118 119  async delete(id: string): Promise<void> {120    for (const table of this.tables.values()) {121      if (table.delete(id)) return;122    }123  }124 125  rows(tableName: string): Row[] {126    return [...this.table(tableName).values()];127  }128 129  seedProduct(doc: {130    projectId: string;131    platform: "IOS" | "Android";132    productId: string;133    billingPeriod?: string;134  }): void {135    this.table("products").set(`products_${++this.counter}`, {136      _id: `products_${this.counter}`,137      _creationTime: Date.now(),138      ...doc,139    });140  }141}142 143function makeCtx(db: MemDb) {144  return { db } as unknown as Parameters<145    typeof recordVerifiedSubscriptionHandler146  >[0];147}148 149const PROJECT_ID = "projects_seed_1";150const TOKEN = "purchase_token_1";151 152describe("entitlement events across the expiry deadline", () => {153  it.each([false, true])(154    "revokes once when the clock expires before notifications (cancel first: %s)",155    async (cancelFirst) => {156      vi.useFakeTimers();157      try {158        vi.setSystemTime(1_000);159        const db = new MemDb();160        const started = await seedWebhookEvent(db, {161          type: "SubscriptionStarted",162          notificationId: "clock-start",163          occurredAt: 1_000,164        });165        await db.patch(started, { expiresAt: 2_000, renewsAt: 2_000 });166        await applySubscriptionEventHandler(makeCtx(db), {167          projectId: PROJECT_ID as never,168          eventId: started as never,169        });170        await bindSubscriptionToUserHandler(makeCtx(db), {171          projectId: PROJECT_ID as never,172          purchaseToken: TOKEN,173          userId: "clock-user",174        });175        vi.setSystemTime(3_000);176        if (cancelFirst) {177          const canceled = await seedWebhookEvent(db, {178            type: "SubscriptionStarted",179            notificationId: "clock-cancel",180            occurredAt: 2_500,181          });182          await db.patch(canceled, {183            type: "SubscriptionCanceled",184            expiresAt: 2_000,185            renewsAt: undefined,186            willRenew: false,187          });188          await applySubscriptionEventHandler(makeCtx(db), {189            projectId: PROJECT_ID as never,190            eventId: canceled as never,191          });192        }193        const expired = await seedWebhookEvent(db, {194          type: "SubscriptionExpired",195          notificationId: "clock-expire",196          occurredAt: 3_000,197        });198        await db.patch(expired, { expiresAt: 2_000 });199        const args = {200          projectId: PROJECT_ID as never,201          eventId: expired as never,202        };203        await applySubscriptionEventHandler(makeCtx(db), args);204        await applySubscriptionEventHandler(makeCtx(db), args);205        expect(206          db207            .rows("commerceEvents")208            .filter((row) => String(row.eventType).startsWith("entitlement."))209            .map((row) => row.eventType),210        ).toEqual(["entitlement.granted", "entitlement.revoked"]);211      } finally {212        vi.useRealTimers();213      }214    },215  );216});217 218async function seedWebhookEvent(219  db: MemDb,220  args: {221    type:222      | "SubscriptionStarted"223      | "SubscriptionRenewed"224      | "SubscriptionExpired"225      | "SubscriptionProductChanged";226    notificationId: string;227    occurredAt: number;228    platform?: "IOS" | "Android";229    productId?: string;230  },231): Promise<string> {232  const platform = args.platform ?? "Android";233  return await db.insert("webhookEvents", {234    projectId: PROJECT_ID,235    type: args.type,236    source:237      platform === "IOS"238        ? "AppleAppStoreServerNotificationsV2"239        : "GooglePlayRealTimeDeveloperNotifications",240    platform,241    environment: "Sandbox",242    purchaseToken: TOKEN,243    sourceNotificationId: args.notificationId,244    productId: args.productId ?? "premium_monthly",245    subscriptionState:246      args.type === "SubscriptionExpired" ? "Expired" : "Active",247    expiresAt: 1_800_000_000_000,248    renewsAt: 1_800_000_000_000,249    currency: "USD",250    priceAmountMicros: 9_990_000,251    occurredAt: args.occurredAt,252    receivedAt: args.occurredAt,253  });254}255 256describe("subscription erasure across replacement tokens", () => {257  it.each([false, true])(258    "reconciles a granted successor without restoring erased identities (successor erasure requested: %s)",259    async (successorErased) => {260      const db = new MemDb();261      const ctx = makeCtx(db);262      const hashKey = "erasure-test-key";263      await db.patch(PROJECT_ID, { userErasureHashKey: hashKey });264      async function erasureJob(userId: string): Promise<string> {265        return db.insert("subscriptionUserErasureJobs", {266          projectId: PROJECT_ID,267          userId,268          userIdHash: await hmacSha256Hex(hashKey, userId),269          status: "queued",270          subscriptionsErased: 0,271          commerceEventsErased: 0,272        });273      }274      await recordVerifiedSubscriptionHandler(ctx, {275        projectId: PROJECT_ID as never,276        platform: "Android",277        purchaseToken: TOKEN,278        productId: "premium_monthly",279        purchaseState: "ENTITLED",280      });281      await bindSubscriptionToUserHandler(ctx, {282        projectId: PROJECT_ID as never,283        purchaseToken: TOKEN,284        userId: "erased-owner",285      });286      await drainSubscriptionUserErasurePage(287        ctx,288        (await erasureJob("erased-owner")) as never,289      );290      const startedId = await seedWebhookEvent(db, {291        type: "SubscriptionStarted",292        notificationId: "successor-granted",293        occurredAt: 1_000,294      });295      await db.patch(startedId, {296        purchaseToken: "replacement",297        expiresAt: Date.now() + 60_000,298      });299      await applySubscriptionEventHandler(ctx, {300        projectId: PROJECT_ID as never,301        eventId: startedId as never,302      });303      await bindSubscriptionToUserHandler(ctx, {304        projectId: PROJECT_ID as never,305        purchaseToken: "replacement",306        userId: "successor-owner",307      });308      const grant = db309        .rows("commerceEvents")310        .find((event) => event.eventType === "entitlement.granted");311      expect(grant?.userId).toBe("successor-owner");312 313      const successorErasureJob = successorErased314        ? await erasureJob("successor-owner")315        : undefined;316      // A destination exists so an emitted revocation would be delivered;317      // none is, which is the point of the assertions below.318      await db.insert("outboundDestinations", {319        projectId: PROJECT_ID,320        enabled: true,321        eventTypes: ["entitlement.revoked"],322      });323      const linkedId = await seedWebhookEvent(db, {324        type: "SubscriptionProductChanged",325        notificationId: "late-erased-link",326        occurredAt: 2_000,327      });328      await db.patch(linkedId, {329        purchaseToken: "replacement",330        linkedPurchaseToken: TOKEN,331      });332      const args = {333        projectId: PROJECT_ID as never,334        eventId: linkedId as never,335      };336      await applySubscriptionEventHandler(ctx, args);337      await applySubscriptionEventHandler(ctx, args);338 339      // The erased owner was unlinked, not the subscription. The successor340      // bound the replacement token afterwards, so that live association341      // survives the merge and nothing is revoked.342      expect(db.rows("subscriptions")).toHaveLength(1);343      expect(db.rows("subscriptions")[0].accountErased).toBeUndefined();344      expect(db.rows("subscriptions")[0].userId).toBe("successor-owner");345      expect(346        db347          .rows("commerceEvents")348          .filter((event) => event.eventType === "entitlement.revoked"),349      ).toEqual([]);350      expect(db.rows("outboundDeliveries")).toEqual([]);351      if (successorErased) {352        await drainSubscriptionUserErasurePage(353          ctx,354          successorErasureJob as never,355        );356        expect(357          db.rows("commerceEvents").some((event) => event.userId !== undefined),358        ).toBe(false);359      }360      expect(361        JSON.stringify(362          [...db.tables.values()].map((rows) => [...rows.values()]),363        ),364      ).not.toContain("erased-owner");365    },366  );367 368  it.each([369    { erasedToken: TOKEN, successorExists: false, boundToken: undefined },370    { erasedToken: TOKEN, successorExists: true, boundToken: undefined },371    { erasedToken: TOKEN, successorExists: true, boundToken: "replacement" },372    { erasedToken: "replacement", successorExists: true, boundToken: TOKEN },373  ])(374    "merges an erased record without dropping a live binding: %j",375    async (testCase) => {376      const db = new MemDb();377      const ctx = makeCtx(db);378      for (const purchaseToken of testCase.successorExists379        ? [TOKEN, "replacement"]380        : [TOKEN]) {381        const id = await recordVerifiedSubscriptionHandler(ctx, {382          projectId: PROJECT_ID as never,383          platform: "Android",384          purchaseToken,385          productId: "premium_monthly",386          purchaseState: "ENTITLED",387          subscriptionState: "SUBSCRIPTION_STATE_ACTIVE",388        });389        if (purchaseToken === testCase.erasedToken)390          await db.patch(id!, { accountErased: true });391        if (purchaseToken === testCase.boundToken)392          await bindSubscriptionToUserHandler(ctx, {393            projectId: PROJECT_ID as never,394            purchaseToken,395            userId: "linked-owner",396          });397      }398      const eventId = await seedWebhookEvent(db, {399        type: "SubscriptionStarted",400        notificationId: "erased-replacement",401        occurredAt: Date.now(),402      });403      await db.patch(eventId, {404        purchaseToken: "replacement",405        linkedPurchaseToken: TOKEN,406      });407      await applySubscriptionEventHandler(ctx, {408        projectId: PROJECT_ID as never,409        eventId: eventId as never,410      });411 412      expect(db.rows("subscriptions")).toHaveLength(1);413      const merged = db.rows("subscriptions")[0];414      expect(merged.purchaseToken).toBe("replacement");415      // The merge must not attribute the record to anyone who did not bind it,416      // and it must not revoke the binder who did.417      expect(418        db419          .rows("commerceEvents")420          .every(421            (event) =>422              event.userId === undefined || event.userId === "linked-owner",423          ),424      ).toBe(true);425      expect(426        db427          .rows("commerceEvents")428          .some((event) => event.eventType === "entitlement.revoked"),429      ).toBe(false);430      if (testCase.boundToken) {431        // A live binding on either side is a later association than the erasure,432        // so it survives and the marker does not carry.433        expect(merged.userId).toBe("linked-owner");434        expect(merged.accountErased).toBeUndefined();435      } else {436        expect(merged.userId).toBeUndefined();437        expect(merged.accountErased).toBe(true);438        // Unowned again, so the record can be associated with someone new.439        expect(440          await bindSubscriptionToUserHandler(ctx, {441            projectId: PROJECT_ID as never,442            purchaseToken: "replacement",443            userId: "new-owner",444          }),445        ).not.toBeNull();446        expect(db.rows("subscriptions")[0].accountErased).toBeUndefined();447      }448    },449  );450});451 452describe("bindSubscriptionToUser amount handling", () => {453  it("does not repeat an amount the webhook already reported", async () => {454    const db = new MemDb();455    db.seedProduct({456      projectId: PROJECT_ID,457      platform: "Android",458      productId: "premium_monthly",459      billingPeriod: "P1M",460    });461    const eventId = await seedWebhookEvent(db, {462      type: "SubscriptionStarted",463      notificationId: "message-webhook-first",464      occurredAt: 1_000,465    });466    await applySubscriptionEventHandler(makeCtx(db), {467      projectId: PROJECT_ID as never,468      eventId: eventId as never,469    });470    const afterWebhook = db471      .rows("commerceEvents")472      .filter((row) => row.amountMicros !== undefined).length;473 474    await bindSubscriptionToUserHandler(makeCtx(db), {475      projectId: PROJECT_ID as never,476      purchaseToken: TOKEN,477      userId: "user_1",478    });479 480    const priced = db481      .rows("commerceEvents")482      .filter((row) => row.amountMicros !== undefined);483    // The bind grant correlates an existing purchase to a user; it is not a484    // second billing, so the count must not move.485    expect(priced.length).toBe(afterWebhook);486    expect(db.rows("commerceEvents").at(-1)?.eventType).toBe(487      "entitlement.granted",488    );489    expect(db.rows("commerceEvents").at(-1)?.amountMicros).toBeUndefined();490  });491});492 493describe("applySubscriptionEventHandler", () => {494  beforeEach(() => {495    vi.setSystemTime(new Date("2026-01-01T00:00:00.000Z"));496  });497 498  afterEach(() => {499    vi.useRealTimers();500  });501 502  // Commerce Protocol's `whenNoPriorStoreEvent` turns on store history, not on503  // whether a record exists — a purchase learned from a client receipt but504  // never from the store still begins the story. That distinction lives here,505  // in the override, and nowhere in the state machine, so it is tested here.506  it("starts rather than recovers when a record exists with no store history", async () => {507    const db = new MemDb();508    db.seedProduct({509      projectId: PROJECT_ID,510      platform: "Android",511      productId: "premium_monthly",512      billingPeriod: "P1M",513    });514    // A client receipt created this row; no store notification ever touched it,515    // so it carries no lastEventId.516    await db.insert("subscriptions", {517      projectId: PROJECT_ID,518      platform: "Android",519      purchaseToken: TOKEN,520      productId: "premium_monthly",521      state: "Unknown",522      willRenew: true,523      updatedAt: 0,524    });525    const eventId = await seedWebhookEvent(db, {526      type: "SubscriptionStarted",527      notificationId: "message-first-store-event",528      occurredAt: 1_000,529    });530 531    await applySubscriptionEventHandler(makeCtx(db), {532      projectId: PROJECT_ID as never,533      eventId: eventId as never,534    });535    // Assert on what was emitted, not what the handler returned: the override536    // applies to the commerce event, while the return value carries the state537    // machine's own transition.538    expect(db.rows("commerceEvents").map((row) => row.eventType)).toContain(539      "subscription.started",540    );541  });542 543  it.each([["SubscriptionPriceChange"], ["SubscriptionDeferred"]])(544    "emits no commerce event when %s is the first store event for a receipt-bootstrapped row",545    async (storeEventType) => {546      // store-event-mapping.json pins whenNoPriorStoreEvent -> event: null for547      // price changes and deferrals: with no earlier store event there is no548      // baseline the event could describe.549      const db = new MemDb();550      db.seedProduct({551        projectId: PROJECT_ID,552        platform: "Android",553        productId: "premium_monthly",554        billingPeriod: "P1M",555      });556      await db.insert("subscriptions", {557        projectId: PROJECT_ID,558        platform: "Android",559        purchaseToken: TOKEN,560        productId: "premium_monthly",561        state: "Active",562        expiresAt: 9_999_999_999_999,563        willRenew: true,564        updatedAt: 0,565      });566      const eventId = await seedWebhookEvent(db, {567        type: storeEventType as never,568        notificationId: `message-${storeEventType}`,569        occurredAt: 1_000,570      });571 572      await applySubscriptionEventHandler(makeCtx(db), {573        projectId: PROJECT_ID as never,574        eventId: eventId as never,575      });576      expect(db.rows("commerceEvents")).toEqual([]);577    },578  );579 580  // Rows written before `lastEventOccurredAt` existed carry only `lastEventId`,581  // so the timestamp guard cannot judge them. Both of its fallbacks protect a582  // money path: a redelivery re-applies the transition and books it twice.583  it.each([584    ["the same event redelivered", "message-legacy", 500, "Expired"],585    ["an older event arriving late", "message-older", 200, "Expired"],586  ])(587    "drops %s against a legacy row",588    async (_name, notificationId, occurredAt, expected) => {589      const db = new MemDb();590      db.seedProduct({591        projectId: PROJECT_ID,592        platform: "Android",593        productId: "premium_monthly",594        billingPeriod: "P1M",595      });596      const priorId = await seedWebhookEvent(db, {597        type: "SubscriptionExpired",598        notificationId: "message-legacy",599        occurredAt: 500,600      });601      await db.insert("subscriptions", {602        projectId: PROJECT_ID,603        platform: "Android",604        purchaseToken: TOKEN,605        productId: "premium_monthly",606        state: "Expired",607        willRenew: false,608        lastEventId: priorId,609        updatedAt: 0,610      });611      const replayId =612        notificationId === "message-legacy"613          ? priorId614          : await seedWebhookEvent(db, {615              type: "SubscriptionStarted",616              notificationId,617              occurredAt,618            });619 620      await expect(621        applySubscriptionEventHandler(makeCtx(db), {622          projectId: PROJECT_ID as never,623          eventId: replayId as never,624        }),625      ).resolves.toMatchObject({ transition: null });626      expect(db.rows("subscriptions")).toMatchObject([{ state: expected }]);627      expect(db.rows("commerceEvents")).toEqual([]);628    },629  );630 631  it("recovers when the record already has store history", async () => {632    const db = new MemDb();633    db.seedProduct({634      projectId: PROJECT_ID,635      platform: "Android",636      productId: "premium_monthly",637      billingPeriod: "P1M",638    });639    const priorId = await seedWebhookEvent(db, {640      type: "SubscriptionExpired",641      notificationId: "message-prior",642      occurredAt: 500,643    });644    await db.insert("subscriptions", {645      projectId: PROJECT_ID,646      platform: "Android",647      purchaseToken: TOKEN,648      productId: "premium_monthly",649      state: "Expired",650      willRenew: false,651      lastEventId: priorId,652      updatedAt: 0,653    });654    const eventId = await seedWebhookEvent(db, {655      type: "SubscriptionStarted",656      notificationId: "message-after-history",657      occurredAt: 1_000,658    });659 660    await applySubscriptionEventHandler(makeCtx(db), {661      projectId: PROJECT_ID as never,662      eventId: eventId as never,663    });664    expect(db.rows("commerceEvents").map((row) => row.eventType)).toContain(665      "subscription.recovered",666    );667  });668 669  it("applies a recorded-but-unapplied event exactly once on redelivery", async () => {670    const db = new MemDb();671    db.seedProduct({672      projectId: PROJECT_ID,673      platform: "Android",674      productId: "premium_monthly",675      billingPeriod: "P1M",676    });677    const eventId = await seedWebhookEvent(db, {678      type: "SubscriptionStarted",679      notificationId: "message-a",680      occurredAt: 1_000,681    });682    const args = {683      projectId: PROJECT_ID as never,684      eventId: eventId as never,685    };686 687    await expect(688      applySubscriptionEventHandler(makeCtx(db), args),689    ).resolves.toMatchObject({ transition: "Started", active: true });690    const appliedAt = db.rows("webhookEvents")[0]?.appliedAt;691 692    await expect(693      applySubscriptionEventHandler(makeCtx(db), args),694    ).resolves.toMatchObject({ transition: null, active: true });695    expect(db.rows("webhookEvents")[0]?.appliedAt).toBe(appliedAt);696    expect(db.rows("subscriptions")).toMatchObject([697      { state: "Active", lastEventId: eventId },698    ]);699    expect(db.rows("subscriptionStats")).toMatchObject([700      { activeSubs: 1, mrrMicros: 9_990_000 },701    ]);702  });703 704  it("applies distinct same-timestamp events without replaying the old one", async () => {705    const db = new MemDb();706    db.seedProduct({707      projectId: PROJECT_ID,708      platform: "Android",709      productId: "premium_monthly",710      billingPeriod: "P1M",711    });712    const startedId = await seedWebhookEvent(db, {713      type: "SubscriptionStarted",714      notificationId: "message-a",715      occurredAt: 1_000,716    });717    const expiredId = await seedWebhookEvent(db, {718      type: "SubscriptionExpired",719      notificationId: "message-b",720      occurredAt: 1_000,721    });722 723    await applySubscriptionEventHandler(makeCtx(db), {724      projectId: PROJECT_ID as never,725      eventId: startedId as never,726    });727    await applySubscriptionEventHandler(makeCtx(db), {728      projectId: PROJECT_ID as never,729      eventId: expiredId as never,730    });731    await expect(732      applySubscriptionEventHandler(makeCtx(db), {733        projectId: PROJECT_ID as never,734        eventId: startedId as never,735      }),736    ).resolves.toMatchObject({ transition: null, active: false });737 738    expect(db.rows("subscriptions")).toMatchObject([739      { state: "Expired", lastEventId: expiredId },740    ]);741    expect(db.rows("subscriptionStats")).toMatchObject([742      { activeSubs: 0, mrrMicros: 0 },743    ]);744  });745 746  it("uses ingestion order to backfill a same-timestamp legacy event", async () => {747    const db = new MemDb();748    db.seedProduct({749      projectId: PROJECT_ID,750      platform: "Android",751      productId: "premium_monthly",752      billingPeriod: "P1M",753    });754    const startedId = await seedWebhookEvent(db, {755      type: "SubscriptionStarted",756      notificationId: "legacy-a",757      occurredAt: 1_000,758    });759    const expiredId = await seedWebhookEvent(db, {760      type: "SubscriptionExpired",761      notificationId: "legacy-b",762      occurredAt: 1_000,763    });764    await applySubscriptionEventHandler(makeCtx(db), {765      projectId: PROJECT_ID as never,766      eventId: startedId as never,767    });768    await applySubscriptionEventHandler(makeCtx(db), {769      projectId: PROJECT_ID as never,770      eventId: expiredId as never,771    });772    await db.patch(startedId, { appliedAt: undefined });773 774    await expect(775      applySubscriptionEventHandler(makeCtx(db), {776        projectId: PROJECT_ID as never,777        eventId: startedId as never,778      }),779    ).resolves.toMatchObject({ transition: null, active: false });780 781    expect(782      db.rows("webhookEvents").find((row) => row._id === startedId),783    ).toHaveProperty("appliedAt", Date.now());784    expect(db.rows("subscriptions")).toMatchObject([785      { state: "Expired", lastEventId: expiredId },786    ]);787    expect(db.rows("subscriptionStats")).toMatchObject([788      { activeSubs: 0, mrrMicros: 0 },789    ]);790  });791 792  it("keeps durable ordering after the previous webhook row is pruned", async () => {793    const db = new MemDb();794    const startedId = await seedWebhookEvent(db, {795      type: "SubscriptionStarted",796      notificationId: "newer",797      occurredAt: 2_000,798    });799    await applySubscriptionEventHandler(makeCtx(db), {800      projectId: PROJECT_ID as never,801      eventId: startedId as never,802    });803    await db.delete(startedId);804    const staleId = await seedWebhookEvent(db, {805      type: "SubscriptionExpired",806      notificationId: "older",807      occurredAt: 1_000,808    });809 810    await expect(811      applySubscriptionEventHandler(makeCtx(db), {812        projectId: PROJECT_ID as never,813        eventId: staleId as never,814      }),815    ).resolves.toMatchObject({ transition: null, active: true });816    expect(db.rows("subscriptions")).toMatchObject([817      {818        state: "Active",819        lastEventOccurredAt: 2_000,820        lastEventSourceNotificationId: "newer",821      },822    ]);823  });824 825  it("does not grant entitlement for an already-expired start event", async () => {826    const db = new MemDb();827    const eventId = await seedWebhookEvent(db, {828      type: "SubscriptionStarted",829      notificationId: "expired-start",830      occurredAt: 1_000,831    });832    await db.patch(eventId, { expiresAt: Date.now() - 1 });833 834    await expect(835      applySubscriptionEventHandler(makeCtx(db), {836        projectId: PROJECT_ID as never,837        eventId: eventId as never,838      }),839    ).resolves.toMatchObject({ transition: "Started", active: false });840    expect(db.rows("commerceEvents").map((row) => row.eventType)).toEqual([841      "subscription.started",842    ]);843  });844 845  it("records one-time events without creating subscription commerce", async () => {846    const db = new MemDb();847    const eventId = await seedWebhookEvent(db, {848      type: "SubscriptionStarted",849      notificationId: "one-time",850      occurredAt: 1_000,851    });852    await db.patch(eventId, { productKind: "one_time" });853 854    await expect(855      applySubscriptionEventHandler(makeCtx(db), {856        projectId: PROJECT_ID as never,857        eventId: eventId as never,858      }),859    ).resolves.toEqual({ transition: null, active: false });860    expect(db.rows("subscriptions")).toHaveLength(0);861    expect(db.rows("commerceEvents")).toHaveLength(0);862  });863 864  it("keeps an Apple renewal preference separate from the active product", async () => {865    const db = new MemDb();866    db.seedProduct({867      projectId: PROJECT_ID,868      platform: "IOS",869      productId: "premium_monthly",870      billingPeriod: "P1M",871    });872    const startedId = await seedWebhookEvent(db, {873      type: "SubscriptionStarted",874      notificationId: "apple-started",875      occurredAt: 1_000,876      platform: "IOS",877    });878    await applySubscriptionEventHandler(makeCtx(db), {879      projectId: PROJECT_ID as never,880      eventId: startedId as never,881    });882    const changedId = await seedWebhookEvent(db, {883      type: "SubscriptionProductChanged",884      notificationId: "apple-next-product",885      occurredAt: 2_000,886      platform: "IOS",887      productId: "premium_yearly",888    });889    await db.patch(changedId, { priceAmountMicros: 99_990_000 });890 891    await expect(892      applySubscriptionEventHandler(makeCtx(db), {893        projectId: PROJECT_ID as never,894        eventId: changedId as never,895      }),896    ).resolves.toMatchObject({ transition: "ProductChanged", active: true });897    expect(db.rows("subscriptions")).toMatchObject([898      {899        productId: "premium_monthly",900        platform: "IOS",901        priceAmountMicros: 9_990_000,902      },903    ]);904    expect(db.rows("commerceEvents").at(-1)).toMatchObject({905      eventType: "subscription.product_changed",906      productId: "premium_yearly",907      amountMicros: 99_990_000,908      subscription: { productId: "premium_monthly" },909    });910    await db.delete(changedId);911 912    await bindSubscriptionToUserHandler(makeCtx(db), {913      projectId: PROJECT_ID as never,914      purchaseToken: TOKEN,915      userId: "user-1",916    });917    expect(db.rows("commerceEvents").at(-1)).toMatchObject({918      eventType: "entitlement.granted",919      productId: "premium_monthly",920      userId: "user-1",921    });922    expect(db.rows("commerceEvents").at(-1)?.amountMicros).toBeUndefined();923 924    const renewedId = await seedWebhookEvent(db, {925      type: "SubscriptionRenewed",926      notificationId: "apple-renewed-on-new-product",927      occurredAt: 3_000,928      platform: "IOS",929      productId: "premium_yearly",930    });931    await applySubscriptionEventHandler(makeCtx(db), {932      projectId: PROJECT_ID as never,933      eventId: renewedId as never,934    });935    expect(db.rows("commerceEvents").at(-1)).toMatchObject({936      eventType: "subscription.renewed",937      productId: "premium_yearly",938      previousProductId: "premium_monthly",939      userId: "user-1",940    });941  });942 943  it("applies an Apple upgrade product immediately", async () => {944    const db = new MemDb();945    const startedId = await seedWebhookEvent(db, {946      type: "SubscriptionStarted",947      notificationId: "apple-started",948      occurredAt: 1_000,949      platform: "IOS",950    });951    await applySubscriptionEventHandler(makeCtx(db), {952      projectId: PROJECT_ID as never,953      eventId: startedId as never,954    });955    const changedId = await seedWebhookEvent(db, {956      type: "SubscriptionProductChanged",957      notificationId: "apple-upgrade",958      occurredAt: 2_000,959      platform: "IOS",960      productId: "premium_yearly",961    });962    await db.patch(changedId, { effectiveImmediately: true });963 964    await applySubscriptionEventHandler(makeCtx(db), {965      projectId: PROJECT_ID as never,966      eventId: changedId as never,967    });968 969    expect(db.rows("subscriptions")[0]).toMatchObject({970      productId: "premium_yearly",971      lastEventId: changedId,972    });973  });974 975  it("includes the previous product for an applied Google item change", async () => {976    const db = new MemDb();977    const startedId = await seedWebhookEvent(db, {978      type: "SubscriptionStarted",979      notificationId: "google-started",980      occurredAt: 1_000,981    });982    await applySubscriptionEventHandler(makeCtx(db), {983      projectId: PROJECT_ID as never,984      eventId: startedId as never,985    });986    const changedId = await seedWebhookEvent(db, {987      type: "SubscriptionProductChanged",988      notificationId: "google-item-change",989      occurredAt: 2_000,990      productId: "premium_yearly",991    });992    await applySubscriptionEventHandler(makeCtx(db), {993      projectId: PROJECT_ID as never,994      eventId: changedId as never,995    });996 997    expect(db.rows("commerceEvents").at(-1)).toMatchObject({998      eventType: "subscription.product_changed",999      previousProductId: "premium_monthly",1000      productId: "premium_yearly",1001      subscription: { productId: "premium_yearly" },1002    });1003  });1004 1005  it("does not guess a product for a multi-item Google change", async () => {1006    const db = new MemDb();1007    const startedId = await seedWebhookEvent(db, {1008      type: "SubscriptionStarted",1009      notificationId: "google-bundle-started",1010      occurredAt: 1_000,1011    });1012    await applySubscriptionEventHandler(makeCtx(db), {1013      projectId: PROJECT_ID as never,1014      eventId: startedId as never,1015    });1016    const changedId = await db.insert("webhookEvents", {1017      projectId: PROJECT_ID,1018      type: "SubscriptionProductChanged",1019      source: "GooglePlayRealTimeDeveloperNotifications",1020      platform: "Android",1021      environment: "Production",1022      purchaseToken: TOKEN,1023      productKind: "subscription",1024      sourceNotificationId: "google-bundle-change",1025      occurredAt: 2_000,1026      receivedAt: 2_000,1027    });1028 1029    await expect(1030      applySubscriptionEventHandler(makeCtx(db), {1031        projectId: PROJECT_ID as never,1032        eventId: changedId as never,1033      }),1034    ).resolves.toMatchObject({ transition: null, active: true });1035    expect(db.rows("subscriptions")[0]).toMatchObject({1036      productId: "premium_monthly",1037      lastEventId: changedId,1038      lastEventOccurredAt: 2_000,1039    });1040    expect(db.rows("commerceEvents")).toHaveLength(1);1041 1042    const delayedId = await seedWebhookEvent(db, {1043      type: "SubscriptionExpired",1044      notificationId: "google-delayed-expiry",1045      occurredAt: 1_500,1046    });1047    await expect(1048      applySubscriptionEventHandler(makeCtx(db), {1049        projectId: PROJECT_ID as never,1050        eventId: delayedId as never,1051      }),1052    ).resolves.toMatchObject({ transition: null, active: true });1053    expect(db.rows("subscriptions")[0]).toMatchObject({1054      state: "Active",1055      lastEventId: changedId,1056    });1057  });1058 1059  it("moves a Google replacement flow onto the linked token row", async () => {1060    const db = new MemDb();1061    const startedId = await seedWebhookEvent(db, {1062      type: "SubscriptionStarted",1063      notificationId: "google-old-token",1064      occurredAt: 1_000,1065    });1066    await applySubscriptionEventHandler(makeCtx(db), {1067      projectId: PROJECT_ID as never,1068      eventId: startedId as never,1069    });1070    const changedId = await db.insert("webhookEvents", {1071      projectId: PROJECT_ID,1072      type: "SubscriptionProductChanged",1073      source: "GooglePlayRealTimeDeveloperNotifications",1074      platform: "Android",1075      environment: "Production",1076      purchaseToken: "purchase_token_2",1077      linkedPurchaseToken: TOKEN,1078      productKind: "subscription",1079      productId: "premium_yearly",1080      subscriptionState: "Active",1081      sourceNotificationId: "google-new-token",1082      occurredAt: 2_000,1083      receivedAt: 2_000,1084    });1085    await applySubscriptionEventHandler(makeCtx(db), {1086      projectId: PROJECT_ID as never,1087      eventId: changedId as never,1088    });1089 1090    expect(db.rows("subscriptions")).toHaveLength(1);1091    expect(db.rows("subscriptions")[0]).toMatchObject({1092      purchaseToken: "purchase_token_2",1093      productId: "premium_yearly",1094    });1095    expect(db.rows("commerceEvents").at(-1)).toMatchObject({1096      eventType: "subscription.product_changed",1097      previousProductId: "premium_monthly",1098      productId: "premium_yearly",1099    });1100  });1101 1102  it("classifies a linked Google purchase with a new product as a change", async () => {1103    const db = new MemDb();1104    const startedId = await seedWebhookEvent(db, {1105      type: "SubscriptionStarted",1106      notificationId: "google-linked-start-old",1107      occurredAt: 1_000,1108    });1109    await applySubscriptionEventHandler(makeCtx(db), {1110      projectId: PROJECT_ID as never,1111      eventId: startedId as never,1112    });1113    const replacementId = await db.insert("webhookEvents", {1114      projectId: PROJECT_ID,1115      type: "SubscriptionStarted",1116      source: "GooglePlayRealTimeDeveloperNotifications",1117      platform: "Android",1118      environment: "Production",1119      purchaseToken: "purchase_token_2",1120      linkedPurchaseToken: TOKEN,1121      productKind: "subscription",1122      productId: "premium_yearly",1123      subscriptionState: "Active",1124      sourceNotificationId: "google-linked-start-new",1125      occurredAt: 2_000,1126      receivedAt: 2_000,1127    });1128 1129    await expect(1130      applySubscriptionEventHandler(makeCtx(db), {1131        projectId: PROJECT_ID as never,1132        eventId: replacementId as never,1133      }),1134    ).resolves.toMatchObject({ transition: "ProductChanged", active: true });1135 1136    expect(1137      db1138        .rows("commerceEvents")1139        .filter((event) => event.sourceEventId === replacementId),1140    ).toEqual([1141      expect.objectContaining({1142        eventType: "subscription.product_changed",1143        previousProductId: "premium_monthly",1144        productId: "premium_yearly",1145      }),1146    ]);1147  });1148 1149  it("defers a same-product token handoff until the replacement renews", async () => {1150    const db = new MemDb();1151    const startedId = await seedWebhookEvent(db, {1152      type: "SubscriptionStarted",1153      notificationId: "google-deferred-start-old",1154      occurredAt: 1_000,1155    });1156    await applySubscriptionEventHandler(makeCtx(db), {1157      projectId: PROJECT_ID as never,1158      eventId: startedId as never,1159    });1160    const handoffId = await db.insert("webhookEvents", {1161      projectId: PROJECT_ID,1162      type: "SubscriptionStarted",1163      source: "GooglePlayRealTimeDeveloperNotifications",1164      platform: "Android",1165      environment: "Production",1166      purchaseToken: "purchase_token_2",1167      linkedPurchaseToken: TOKEN,1168      productKind: "subscription",1169      productId: "premium_monthly",1170      subscriptionState: "Active",1171      sourceNotificationId: "google-deferred-token-handoff",1172      occurredAt: 2_000,1173      receivedAt: 2_000,1174    });1175    await expect(1176      applySubscriptionEventHandler(makeCtx(db), {1177        projectId: PROJECT_ID as never,1178        eventId: handoffId as never,1179      }),1180    ).resolves.toMatchObject({ transition: null, active: true });1181    expect(1182      db1183        .rows("commerceEvents")1184        .filter((event) => event.sourceEventId === handoffId),1185    ).toEqual([]);1186 1187    const predecessorExpiryId = await db.insert("webhookEvents", {1188      projectId: PROJECT_ID,1189      type: "SubscriptionExpired",1190      source: "GooglePlayRealTimeDeveloperNotifications",1191      platform: "Android",1192      environment: "Production",1193      purchaseToken: TOKEN,1194      productKind: "subscription",1195      productId: "premium_monthly",1196      subscriptionState: "Expired",1197      expiresAt: 2_500,1198      sourceNotificationId: "google-deferred-predecessor-expired",1199      occurredAt: 2_500,1200      receivedAt: 2_500,1201    });1202    await expect(1203      applySubscriptionEventHandler(makeCtx(db), {1204        projectId: PROJECT_ID as never,1205        eventId: predecessorExpiryId as never,1206      }),1207    ).resolves.toMatchObject({ transition: null, active: true });1208    expect(db.rows("subscriptions")).toHaveLength(1);1209    expect(db.rows("subscriptions")[0]).toMatchObject({1210      purchaseToken: "purchase_token_2",1211      state: "Active",1212    });1213    expect(1214      db1215        .rows("commerceEvents")1216        .filter((event) => event.sourceEventId === predecessorExpiryId),1217    ).toEqual([]);1218    expect(db.rows("subscriptionTokenAliases")).toMatchObject([1219      {1220        purchaseToken: TOKEN,1221        successorPurchaseToken: "purchase_token_2",1222      },1223    ]);1224 1225    const successor = db.rows("subscriptions")[0];1226    const statsBeforeStaleVerification = structuredClone(1227      db.rows("subscriptionStats"),1228    );1229    await expect(1230      recordVerifiedSubscriptionHandler(makeCtx(db), {1231        projectId: PROJECT_ID as never,1232        platform: "Android",1233        purchaseToken: TOKEN,1234        productId: "stale_monthly",1235        purchaseState: "ENTITLED",1236        subscriptionState: "SUBSCRIPTION_STATE_ACTIVE",1237        expiresAt: 2_600,1238        willRenew: false,1239      }),1240    ).resolves.toBe(successor._id);1241    expect(db.rows("subscriptions")).toMatchObject([1242      {1243        _id: successor._id,1244        purchaseToken: "purchase_token_2",1245        productId: "premium_monthly",1246        state: "Active",1247      },1248    ]);1249    expect(db.rows("subscriptionStats")).toEqual(statsBeforeStaleVerification);1250 1251    await expect(1252      bindSubscriptionToUserHandler(makeCtx(db), {1253        projectId: PROJECT_ID as never,1254        purchaseToken: TOKEN,1255        userId: "user-after-handoff",1256      }),1257    ).resolves.toBe(successor._id);1258    expect(db.rows("subscriptions")).toMatchObject([1259      {1260        _id: successor._id,1261        purchaseToken: "purchase_token_2",1262        userId: "user-after-handoff",1263      },1264    ]);1265    expect(db.rows("subscriptions")).toHaveLength(1);1266 1267    const renewalId = await db.insert("webhookEvents", {1268      projectId: PROJECT_ID,1269      type: "SubscriptionRenewed",1270      source: "GooglePlayRealTimeDeveloperNotifications",1271      platform: "Android",1272      environment: "Production",1273      purchaseToken: "purchase_token_2",1274      linkedPurchaseToken: TOKEN,1275      productKind: "subscription",1276      productId: "premium_yearly",1277      subscriptionState: "Active",1278      sourceNotificationId: "google-deferred-first-renewal",1279      occurredAt: 3_000,1280      receivedAt: 3_000,1281    });1282    await applySubscriptionEventHandler(makeCtx(db), {1283      projectId: PROJECT_ID as never,1284      eventId: renewalId as never,1285    });1286 1287    expect(db.rows("subscriptions")[0]).toMatchObject({1288      purchaseToken: "purchase_token_2",1289      productId: "premium_yearly",1290    });1291    expect(1292      db1293        .rows("commerceEvents")1294        .filter((event) => event.sourceEventId === renewalId),1295    ).toEqual([1296      expect.objectContaining({1297        eventType: "subscription.renewed",1298        previousProductId: "premium_monthly",1299        productId: "premium_yearly",1300      }),1301    ]);1302  });1303 1304  it("resolves predecessor aliases beyond 32 replacement hops", async () => {1305    const db = new MemDb();1306    const hopCount = 40;1307    for (let index = 0; index < hopCount; index += 1) {1308      await db.insert("subscriptionTokenAliases", {1309        projectId: PROJECT_ID,1310        purchaseToken: `token_${index}`,1311        successorPurchaseToken: `token_${index + 1}`,1312        predecessorProductId:1313          index === 0 ? "premium_monthly" : "premium_yearly",1314        createdAt: index,1315        updatedAt: index,1316      });1317    }1318    const subscriptionId = await db.insert("subscriptions", {1319      projectId: PROJECT_ID,1320      purchaseToken: `token_${hopCount}`,1321      productKind: "subscription",1322      productId: "premium_yearly",1323      platform: "Android",1324      state: "Active",1325      expiresAt: 1_900_000_000_000,1326      willRenew: true,1327      startedAt: 1,1328      updatedAt: 1,1329    });1330    await db.insert("subscriptions", {1331      projectId: PROJECT_ID,1332      purchaseToken: "token_20",1333      productKind: "subscription",1334      productId: "stale_intermediate",1335      platform: "Android",1336      state: "Expired",1337      expiresAt: 2_000,1338      willRenew: false,1339      startedAt: 1,1340      updatedAt: 1,1341    });1342    const expiredId = await db.insert("webhookEvents", {1343      projectId: PROJECT_ID,1344      type: "SubscriptionExpired",1345      source: "GooglePlayRealTimeDeveloperNotifications",1346      platform: "Android",1347      environment: "Production",1348      purchaseToken: "token_0",1349      productKind: "subscription",1350      productId: "premium_monthly",1351      sourceNotificationId: "old-chain-expired",1352      occurredAt: 2_000,1353      receivedAt: 2_000,1354    });1355 1356    await expect(1357      getSourceProductIdByTokenHandler(makeCtx(db), {1358        projectId: PROJECT_ID as never,1359        purchaseToken: "token_0",1360      }),1361    ).resolves.toBe("premium_monthly");1362    await expect(1363      getCurrentProductIdByTokenHandler(makeCtx(db), {1364        projectId: PROJECT_ID as never,1365        purchaseToken: "token_0",1366      }),1367    ).resolves.toBe("premium_yearly");1368 1369    await expect(1370      applySubscriptionEventHandler(makeCtx(db), {1371        projectId: PROJECT_ID as never,1372        eventId: expiredId as never,1373      }),1374    ).resolves.toEqual({1375      transition: null,1376      active: true,1377      subscriptionId,1378    });1379    expect(db.rows("subscriptions")).toHaveLength(2);1380    expect(1381      db.rows("subscriptions").find((row) => row._id === subscriptionId),1382    ).toMatchObject({1383      purchaseToken: `token_${hopCount}`,1384      productId: "premium_yearly",1385      state: "Active",1386    });1387    expect(db.rows("commerceEvents")).toEqual([]);1388  });1389 1390  it("emits predecessor refunds and revocations without deactivating the successor", async () => {1391    const db = new MemDb();1392    const subscriptionId = await db.insert("subscriptions", {1393      projectId: PROJECT_ID,1394      purchaseToken: "current_token",1395      productKind: "subscription",1396      productId: "premium_yearly",1397      platform: "Android",1398      state: "Active",1399      expiresAt: 1_900_000_000_000,1400      willRenew: true,1401      startedAt: 1,1402      updatedAt: 1,1403    });1404    await db.insert("subscriptionTokenAliases", {1405      projectId: PROJECT_ID,1406      purchaseToken: "old_token",1407      successorPurchaseToken: "current_token",1408      predecessorProductId: "premium_monthly",1409      createdAt: 1,1410      updatedAt: 1,1411    });1412 1413    const cases = [1414      ["PurchaseRefunded", "Refunded", "subscription.refunded"],1415      ["SubscriptionRevoked", "Revoked", "subscription.revoked"],1416    ] as const;1417    for (const [type, transition, eventType] of cases) {1418      const eventId = await db.insert("webhookEvents", {1419        projectId: PROJECT_ID,1420        type,1421        source: "GooglePlayRealTimeDeveloperNotifications",1422        platform: "Android",1423        environment: "Production",1424        purchaseToken: "old_token",1425        productKind: "subscription",1426        productId: "premium_monthly",1427        sourceNotificationId: `old-token-${type}`,1428        occurredAt: 2_000,1429        receivedAt: 2_000,1430      });1431      await expect(1432        applySubscriptionEventHandler(makeCtx(db), {1433          projectId: PROJECT_ID as never,1434          eventId: eventId as never,1435        }),1436      ).resolves.toEqual({ transition, active: true, subscriptionId });1437      expect(1438        db1439          .rows("commerceEvents")1440          .filter((event) => event.sourceEventId === eventId)1441          .map((event) => [1442            event.eventType,1443            event.productId,1444            (event.subscription as { state?: string } | undefined)?.state,1445            event.entitlementActive,1446          ]),1447      ).toEqual([[eventType, "premium_monthly", transition, false]]);1448    }1449    expect(db.rows("subscriptions")).toMatchObject([1450      {1451        purchaseToken: "current_token",1452        productId: "premium_yearly",1453        state: "Active",1454      },1455    ]);1456  });1457 1458  it("classifies a same-product linked prepaid extension as a renewal", async () => {1459    const db = new MemDb();1460    const startedId = await seedWebhookEvent(db, {1461      type: "SubscriptionStarted",1462      notificationId: "google-prepaid-start",1463      occurredAt: 1_000,1464    });1465    await applySubscriptionEventHandler(makeCtx(db), {1466      projectId: PROJECT_ID as never,1467      eventId: startedId as never,1468    });1469    const topUpId = await db.insert("webhookEvents", {1470      projectId: PROJECT_ID,1471      type: "SubscriptionStarted",1472      source: "GooglePlayRealTimeDeveloperNotifications",1473      platform: "Android",1474      environment: "Production",1475      purchaseToken: "purchase_token_2",1476      linkedPurchaseToken: TOKEN,1477      productKind: "subscription",1478      productId: "premium_monthly",1479      subscriptionState: "Active",1480      expiresAt: 1_900_000_000_000,1481      willRenew: false,1482      sourceNotificationId: "google-prepaid-top-up",1483      occurredAt: 2_000,1484      receivedAt: 2_000,1485    });1486 1487    await expect(1488      applySubscriptionEventHandler(makeCtx(db), {1489        projectId: PROJECT_ID as never,1490        eventId: topUpId as never,1491      }),1492    ).resolves.toMatchObject({ transition: "Renewed", active: true });1493    expect(1494      db1495        .rows("commerceEvents")1496        .filter((event) => event.sourceEventId === topUpId),1497    ).toEqual([1498      expect.objectContaining({1499        eventType: "subscription.renewed",1500        productId: "premium_monthly",1501        subscription: expect.objectContaining({1502          expiresAt: 1_900_000_000_000,1503          willRenew: false,1504        }),1505      }),1506    ]);1507    expect(1508      db.rows("commerceEvents").find((event) => event.sourceEventId === topUpId)1509        ?.subscription,1510    ).not.toHaveProperty("renewsAt");1511    expect(db.rows("subscriptions")[0]).toMatchObject({1512      willRenew: false,1513      expiresAt: 1_900_000_000_000,1514    });1515    expect(db.rows("subscriptions")[0]?.renewsAt).toBeUndefined();1516  });1517 1518  it("classifies an active same-product linked resubscribe as uncanceled", async () => {1519    const db = new MemDb();1520    const startedId = await seedWebhookEvent(db, {1521      type: "SubscriptionStarted",1522      notificationId: "google-resubscribe-start",1523      occurredAt: 1_000,1524    });1525    await applySubscriptionEventHandler(makeCtx(db), {1526      projectId: PROJECT_ID as never,1527      eventId: startedId as never,1528    });1529    const canceledId = await db.insert("webhookEvents", {1530      projectId: PROJECT_ID,1531      type: "SubscriptionCanceled",1532      source: "GooglePlayRealTimeDeveloperNotifications",1533      platform: "Android",1534      environment: "Production",1535      purchaseToken: TOKEN,1536      productKind: "subscription",1537      productId: "premium_monthly",1538      subscriptionState: "Active",1539      expiresAt: 1_800_000_000_000,1540      willRenew: false,1541      sourceNotificationId: "google-resubscribe-canceled",1542      occurredAt: 2_000,1543      receivedAt: 2_000,1544    });1545    await applySubscriptionEventHandler(makeCtx(db), {1546      projectId: PROJECT_ID as never,1547      eventId: canceledId as never,1548    });1549    const resubscribedId = await db.insert("webhookEvents", {1550      projectId: PROJECT_ID,1551      type: "SubscriptionStarted",1552      source: "GooglePlayRealTimeDeveloperNotifications",1553      platform: "Android",1554      environment: "Production",1555      purchaseToken: "purchase_token_2",1556      linkedPurchaseToken: TOKEN,1557      productKind: "subscription",1558      productId: "premium_monthly",1559      subscriptionState: "Active",1560      expiresAt: 1_800_000_000_000,1561      willRenew: true,1562      sourceNotificationId: "google-resubscribe-linked",1563      occurredAt: 3_000,1564      receivedAt: 3_000,1565    });1566 1567    await expect(1568      applySubscriptionEventHandler(makeCtx(db), {1569        projectId: PROJECT_ID as never,1570        eventId: resubscribedId as never,1571      }),1572    ).resolves.toMatchObject({ transition: "Uncanceled", active: true });1573    expect(1574      db1575        .rows("commerceEvents")1576        .filter((event) => event.sourceEventId === resubscribedId)1577        .map((event) => event.eventType),1578    ).toEqual(["subscription.uncanceled"]);1579  });1580 1581  it("recovers a linked inactive predecessor regardless of verification order", async () => {1582    const db = new MemDb();1583    const startedId = await seedWebhookEvent(db, {1584      type: "SubscriptionStarted",1585      notificationId: "google-order-start",1586      occurredAt: 1_000,1587    });1588    await applySubscriptionEventHandler(makeCtx(db), {1589      projectId: PROJECT_ID as never,1590      eventId: startedId as never,1591    });1592    const expiredId = await seedWebhookEvent(db, {1593      type: "SubscriptionExpired",1594      notificationId: "google-order-expired",1595      occurredAt: 2_000,1596    });1597    await applySubscriptionEventHandler(makeCtx(db), {1598      projectId: PROJECT_ID as never,1599      eventId: expiredId as never,1600    });1601    await recordVerifiedSubscriptionHandler(makeCtx(db), {1602      projectId: PROJECT_ID as never,1603      platform: "Android",1604      purchaseToken: "purchase_token_2",1605      productId: "premium_monthly",1606      purchaseState: "ENTITLED",1607      subscriptionState: "SUBSCRIPTION_STATE_ACTIVE",1608      expiresAt: 1_900_000_000_000,1609    });1610    const recoveredId = await db.insert("webhookEvents", {1611      projectId: PROJECT_ID,1612      type: "SubscriptionStarted",1613      source: "GooglePlayRealTimeDeveloperNotifications",1614      platform: "Android",1615      environment: "Production",1616      purchaseToken: "purchase_token_2",1617      linkedPurchaseToken: TOKEN,1618      productKind: "subscription",1619      productId: "premium_monthly",1620      subscriptionState: "Active",1621      expiresAt: 1_900_000_000_000,1622      willRenew: true,1623      sourceNotificationId: "google-order-linked",1624      occurredAt: 3_000,1625      receivedAt: 3_000,1626    });1627 1628    await expect(1629      applySubscriptionEventHandler(makeCtx(db), {1630        projectId: PROJECT_ID as never,1631        eventId: recoveredId as never,1632      }),1633    ).resolves.toMatchObject({ transition: "Recovered", active: true });1634    expect(1635      db1636        .rows("commerceEvents")1637        .filter((event) => event.sourceEventId === recoveredId)1638        .map((event) => event.eventType),1639    ).toEqual(["subscription.recovered"]);1640  });1641 1642  it("recovers an inactive predecessor through a linked Google purchase", async () => {1643    const db = new MemDb();1644    const startedId = await seedWebhookEvent(db, {1645      type: "SubscriptionStarted",1646      notificationId: "google-linked-inactive-start",1647      occurredAt: 1_000,1648    });1649    await applySubscriptionEventHandler(makeCtx(db), {1650      projectId: PROJECT_ID as never,1651      eventId: startedId as never,1652    });1653    const expiredId = await seedWebhookEvent(db, {1654      type: "SubscriptionExpired",1655      notificationId: "google-linked-inactive-expired",1656      occurredAt: 2_000,1657    });1658    await applySubscriptionEventHandler(makeCtx(db), {1659      projectId: PROJECT_ID as never,1660      eventId: expiredId as never,1661    });1662    const recoveredId = await db.insert("webhookEvents", {1663      projectId: PROJECT_ID,1664      type: "SubscriptionStarted",1665      source: "GooglePlayRealTimeDeveloperNotifications",1666      platform: "Android",1667      environment: "Production",1668      purchaseToken: "purchase_token_2",1669      linkedPurchaseToken: TOKEN,1670      productKind: "subscription",1671      productId: "premium_monthly",1672      subscriptionState: "Active",1673      sourceNotificationId: "google-linked-inactive-repurchase",1674      occurredAt: 3_000,1675      receivedAt: 3_000,1676    });1677 1678    await expect(1679      applySubscriptionEventHandler(makeCtx(db), {1680        projectId: PROJECT_ID as never,1681        eventId: recoveredId as never,1682      }),1683    ).resolves.toMatchObject({ transition: "Recovered", active: true });1684    expect(1685      db1686        .rows("commerceEvents")1687        .filter((event) => event.sourceEventId === recoveredId)1688        .map((event) => event.eventType),1689    ).toEqual(["subscription.recovered"]);1690  });1691 1692  it("applies a delayed replacement after the predecessor expires", async () => {1693    const db = new MemDb();1694    const startedId = await seedWebhookEvent(db, {1695      type: "SubscriptionStarted",1696      notificationId: "google-predecessor-started",1697      occurredAt: 1_000,1698    });1699    await applySubscriptionEventHandler(makeCtx(db), {1700      projectId: PROJECT_ID as never,1701      eventId: startedId as never,1702    });1703    const expiredId = await seedWebhookEvent(db, {1704      type: "SubscriptionExpired",1705      notificationId: "google-predecessor-expired",1706      occurredAt: 3_000,1707    });1708    await applySubscriptionEventHandler(makeCtx(db), {1709      projectId: PROJECT_ID as never,1710      eventId: expiredId as never,1711    });1712    const changedId = await db.insert("webhookEvents", {1713      projectId: PROJECT_ID,1714      type: "SubscriptionProductChanged",1715      source: "GooglePlayRealTimeDeveloperNotifications",1716      platform: "Android",1717      environment: "Production",1718      purchaseToken: "purchase_token_2",1719      linkedPurchaseToken: TOKEN,1720      productKind: "subscription",1721      productId: "premium_yearly",1722      subscriptionState: "Active",1723      sourceNotificationId: "google-delayed-replacement",1724      occurredAt: 2_000,1725      receivedAt: 4_000,1726    });1727 1728    await applySubscriptionEventHandler(makeCtx(db), {1729      projectId: PROJECT_ID as never,1730      eventId: changedId as never,1731    });1732 1733    expect(db.rows("subscriptions")).toHaveLength(1);1734    expect(db.rows("subscriptions")[0]).toMatchObject({1735      purchaseToken: "purchase_token_2",1736      productId: "premium_yearly",1737      state: "Active",1738      lastEventId: changedId,1739    });1740    expect(1741      db.rows("webhookEvents").find((row) => row._id === changedId),1742    ).toHaveProperty("appliedAt");1743    expect(db.rows("commerceEvents")).toContainEqual(1744      expect.objectContaining({1745        eventType: "subscription.product_changed",1746        previousProductId: "premium_monthly",1747        productId: "premium_yearly",1748      }),1749    );1750  });1751 1752  it("keeps verification-first replacement state over a later predecessor expiry", async () => {1753    const db = new MemDb();1754    db.seedProduct({1755      projectId: PROJECT_ID,1756      platform: "Android",1757      productId: "premium_monthly",1758      billingPeriod: "P1M",1759    });1760    db.seedProduct({1761      projectId: PROJECT_ID,1762      platform: "Android",1763      productId: "premium_yearly",1764      billingPeriod: "P1Y",1765    });1766    const startedId = await seedWebhookEvent(db, {1767      type: "SubscriptionStarted",1768      notificationId: "google-old-token",1769      occurredAt: 1_000,1770    });1771    await applySubscriptionEventHandler(makeCtx(db), {1772      projectId: PROJECT_ID as never,1773      eventId: startedId as never,1774    });1775    await bindSubscriptionToUserHandler(makeCtx(db), {1776      projectId: PROJECT_ID as never,1777      purchaseToken: TOKEN,1778      userId: "user_1",1779    });1780    await recordVerifiedSubscriptionHandler(makeCtx(db), {1781      projectId: PROJECT_ID as never,1782      platform: "Android",1783      purchaseToken: "purchase_token_2",1784      productId: "premium_yearly",1785      purchaseState: "ENTITLED",1786      subscriptionState: "SUBSCRIPTION_STATE_ACTIVE",1787    });1788    expect(db.rows("subscriptions")).toHaveLength(2);1789    expect(1790      db1791        .rows("subscriptionStats")1792        .reduce((total, row) => total + Number(row.activeSubs), 0),1793    ).toBe(2);1794 1795    const expiredId = await db.insert("webhookEvents", {1796      projectId: PROJECT_ID,1797      type: "SubscriptionExpired",1798      source: "GooglePlayRealTimeDeveloperNotifications",1799      platform: "Android",1800      environment: "Production",1801      purchaseToken: TOKEN,1802      productKind: "subscription",1803      productId: "premium_monthly",1804      subscriptionState: "Expired",1805      sourceNotificationId: "google-old-token-expired",1806      occurredAt: 3_000,1807      receivedAt: 3_000,1808    });1809    await applySubscriptionEventHandler(makeCtx(db), {1810      projectId: PROJECT_ID as never,1811      eventId: expiredId as never,1812    });1813 1814    const changedId = await db.insert("webhookEvents", {1815      projectId: PROJECT_ID,1816      type: "SubscriptionProductChanged",1817      source: "GooglePlayRealTimeDeveloperNotifications",1818      platform: "Android",1819      environment: "Production",1820      purchaseToken: "purchase_token_2",1821      linkedPurchaseToken: TOKEN,1822      productKind: "subscription",1823      productId: "premium_yearly",1824      subscriptionState: "Active",1825      sourceNotificationId: "google-new-token",1826      occurredAt: 2_000,1827      receivedAt: 2_000,1828    });1829    await applySubscriptionEventHandler(makeCtx(db), {1830      projectId: PROJECT_ID as never,1831      eventId: changedId as never,1832    });1833 1834    expect(db.rows("subscriptions")).toHaveLength(1);1835    expect(db.rows("subscriptions")[0]).toMatchObject({1836      purchaseToken: "purchase_token_2",1837      productId: "premium_yearly",1838      userId: "user_1",1839      state: "Active",1840      lastEventId: changedId,1841    });1842    expect(1843      db1844        .rows("subscriptionStats")1845        .reduce((total, row) => total + Number(row.activeSubs), 0),1846    ).toBe(1);1847  });1848 1849  it("preserves active predecessor semantics after replacement verification", async () => {1850    const db = new MemDb();1851    const startedId = await seedWebhookEvent(db, {1852      type: "SubscriptionStarted",1853      notificationId: "google-active-predecessor",1854      occurredAt: 1_000,1855    });1856    await applySubscriptionEventHandler(makeCtx(db), {1857      projectId: PROJECT_ID as never,1858      eventId: startedId as never,1859    });1860    await recordVerifiedSubscriptionHandler(makeCtx(db), {1861      projectId: PROJECT_ID as never,1862      platform: "Android",1863      purchaseToken: "purchase_token_2",1864      productId: "premium_yearly",1865      purchaseState: "ENTITLED",1866      subscriptionState: "SUBSCRIPTION_STATE_ACTIVE",1867    });1868    const changedId = await db.insert("webhookEvents", {1869      projectId: PROJECT_ID,1870      type: "SubscriptionProductChanged",1871      source: "GooglePlayRealTimeDeveloperNotifications",1872      platform: "Android",1873      environment: "Production",1874      purchaseToken: "purchase_token_2",1875      linkedPurchaseToken: TOKEN,1876      productKind: "subscription",1877      productId: "premium_yearly",1878      subscriptionState: "Active",1879      sourceNotificationId: "google-verified-replacement",1880      occurredAt: 2_000,1881      receivedAt: 2_000,1882    });1883 1884    await applySubscriptionEventHandler(makeCtx(db), {1885      projectId: PROJECT_ID as never,1886      eventId: changedId as never,1887    });1888 1889    expect(db.rows("subscriptions")).toHaveLength(1);1890    expect(db.rows("subscriptions")[0]).toMatchObject({1891      purchaseToken: "purchase_token_2",1892      productId: "premium_yearly",1893      state: "Active",1894      lastEventId: changedId,1895    });1896    expect(1897      db1898        .rows("commerceEvents")1899        .filter((event) => event.sourceEventId === changedId),1900    ).toEqual([1901      expect.objectContaining({1902        eventType: "subscription.product_changed",1903        previousProductId: "premium_monthly",1904        productId: "premium_yearly",1905      }),1906    ]);1907  });1908 1909  it("emits a verified replacement when multi-item enrichment omits the product", async () => {1910    const db = new MemDb();1911    const startedId = await seedWebhookEvent(db, {1912      type: "SubscriptionStarted",1913      notificationId: "google-active-predecessor-ambiguous",1914      occurredAt: 1_000,1915    });1916    await applySubscriptionEventHandler(makeCtx(db), {1917      projectId: PROJECT_ID as never,1918      eventId: startedId as never,1919    });1920    await recordVerifiedSubscriptionHandler(makeCtx(db), {1921      projectId: PROJECT_ID as never,1922      platform: "Android",1923      purchaseToken: "purchase_token_2",1924      productId: "premium_yearly",1925      purchaseState: "ENTITLED",1926      subscriptionState: "SUBSCRIPTION_STATE_ACTIVE",1927    });1928    const changedId = await db.insert("webhookEvents", {1929      projectId: PROJECT_ID,1930      type: "SubscriptionProductChanged",1931      source: "GooglePlayRealTimeDeveloperNotifications",1932      platform: "Android",1933      environment: "Production",1934      purchaseToken: "purchase_token_2",1935      linkedPurchaseToken: TOKEN,1936      productKind: "subscription",1937      subscriptionState: "Active",1938      sourceNotificationId: "google-verified-ambiguous-replacement",1939      occurredAt: 2_000,1940      receivedAt: 2_000,1941    });1942 1943    await applySubscriptionEventHandler(makeCtx(db), {1944      projectId: PROJECT_ID as never,1945      eventId: changedId as never,1946    });1947 1948    expect(db.rows("subscriptions")).toHaveLength(1);1949    expect(db.rows("subscriptions")[0]).toMatchObject({1950      purchaseToken: "purchase_token_2",1951      productId: "premium_yearly",1952      state: "Active",1953      lastEventId: changedId,1954    });1955    expect(1956      db1957        .rows("commerceEvents")1958        .filter((event) => event.sourceEventId === changedId),1959    ).toEqual([1960      expect.objectContaining({1961        eventType: "subscription.product_changed",1962        previousProductId: "premium_monthly",1963        productId: "premium_yearly",1964      }),1965    ]);1966  });1967 1968  it("rejects linked Google tokens bound to different users", async () => {1969    const db = new MemDb();1970    const startedId = await seedWebhookEvent(db, {1971      type: "SubscriptionStarted",1972      notificationId: "google-old-token",1973      occurredAt: 1_000,1974    });1975    await applySubscriptionEventHandler(makeCtx(db), {1976      projectId: PROJECT_ID as never,1977      eventId: startedId as never,1978    });1979    await bindSubscriptionToUserHandler(makeCtx(db), {1980      projectId: PROJECT_ID as never,1981      purchaseToken: TOKEN,1982      userId: "user_1",1983    });1984    await recordVerifiedSubscriptionHandler(makeCtx(db), {1985      projectId: PROJECT_ID as never,1986      platform: "Android",1987      purchaseToken: "purchase_token_2",1988      productId: "premium_yearly",1989      purchaseState: "ENTITLED",1990      subscriptionState: "SUBSCRIPTION_STATE_ACTIVE",1991    });1992    await bindSubscriptionToUserHandler(makeCtx(db), {1993      projectId: PROJECT_ID as never,1994      purchaseToken: "purchase_token_2",1995      userId: "user_2",1996    });1997    const changedId = await db.insert("webhookEvents", {1998      projectId: PROJECT_ID,1999      type: "SubscriptionProductChanged",2000      source: "GooglePlayRealTimeDeveloperNotifications",2001      platform: "Android",2002      environment: "Production",2003      purchaseToken: "purchase_token_2",2004      linkedPurchaseToken: TOKEN,2005      productKind: "subscription",2006      productId: "premium_yearly",2007      sourceNotificationId: "google-conflicting-token",2008      occurredAt: 2_000,2009      receivedAt: 2_000,2010    });2011 2012    await expect(2013      applySubscriptionEventHandler(makeCtx(db), {2014        projectId: PROJECT_ID as never,2015        eventId: changedId as never,2016      }),2017    ).rejects.toThrow("different users");2018    expect(db.rows("subscriptions")).toHaveLength(2);2019  });2020 2021  it("keeps a newer store-governed replacement row over an older linked event", async () => {2022    const db = new MemDb();2023    const oldStartedId = await seedWebhookEvent(db, {2024      type: "SubscriptionStarted",2025      notificationId: "google-old-token",2026      occurredAt: 1_000,2027    });2028    await applySubscriptionEventHandler(makeCtx(db), {2029      projectId: PROJECT_ID as never,2030      eventId: oldStartedId as never,2031    });2032    const newStartedId = await db.insert("webhookEvents", {2033      projectId: PROJECT_ID,2034      type: "SubscriptionStarted",2035      source: "GooglePlayRealTimeDeveloperNotifications",2036      platform: "Android",2037      environment: "Production",2038      purchaseToken: "purchase_token_2",2039      productKind: "subscription",2040      productId: "premium_monthly",2041      subscriptionState: "Active",2042      sourceNotificationId: "google-new-token-started",2043      occurredAt: 3_000,2044      receivedAt: 3_000,2045    });2046    await applySubscriptionEventHandler(makeCtx(db), {2047      projectId: PROJECT_ID as never,2048      eventId: newStartedId as never,2049    });2050    const olderLinkedId = await db.insert("webhookEvents", {2051      projectId: PROJECT_ID,2052      type: "SubscriptionProductChanged",2053      source: "GooglePlayRealTimeDeveloperNotifications",2054      platform: "Android",2055      environment: "Production",2056      purchaseToken: "purchase_token_2",2057      linkedPurchaseToken: TOKEN,2058      productKind: "subscription",2059      productId: "premium_yearly",2060      subscriptionState: "Active",2061      sourceNotificationId: "google-older-linked-event",2062      occurredAt: 2_000,2063      receivedAt: 4_000,2064    });2065 2066    await applySubscriptionEventHandler(makeCtx(db), {2067      projectId: PROJECT_ID as never,2068      eventId: olderLinkedId as never,2069    });2070 2071    expect(db.rows("subscriptions")).toHaveLength(1);2072    expect(db.rows("subscriptions")[0]).toMatchObject({2073      purchaseToken: "purchase_token_2",2074      productId: "premium_monthly",2075      lastEventId: newStartedId,2076    });2077    expect(2078      db.rows("webhookEvents").find((row) => row._id === olderLinkedId),2079    ).toHaveProperty("appliedAt");2080    expect(db.rows("subscriptionTokenAliases")).toMatchObject([2081      {2082        purchaseToken: TOKEN,2083        successorPurchaseToken: "purchase_token_2",2084      },2085    ]);2086 2087    const latePredecessorExpiryId = await db.insert("webhookEvents", {2088      projectId: PROJECT_ID,2089      type: "SubscriptionExpired",2090      source: "GooglePlayRealTimeDeveloperNotifications",2091      platform: "Android",2092      environment: "Production",2093      purchaseToken: TOKEN,2094      productKind: "subscription",2095      productId: "premium_monthly",2096      subscriptionState: "Expired",2097      sourceNotificationId: "google-late-predecessor-expired",2098      occurredAt: 4_000,2099      receivedAt: 4_000,2100    });2101    await expect(2102      applySubscriptionEventHandler(makeCtx(db), {2103        projectId: PROJECT_ID as never,2104        eventId: latePredecessorExpiryId as never,2105      }),2106    ).resolves.toMatchObject({ transition: null, active: true });2107    expect(db.rows("subscriptions")).toHaveLength(1);2108    expect(2109      db2110        .rows("commerceEvents")2111        .filter((event) => event.sourceEventId === latePredecessorExpiryId),2112    ).toEqual([]);2113  });2114 2115  it("keeps current-token state when the predecessor expires later", async () => {2116    const db = new MemDb();2117    const oldStartedId = await seedWebhookEvent(db, {2118      type: "SubscriptionStarted",2119      notificationId: "google-predecessor-started",2120      occurredAt: 1_000,2121    });2122    await applySubscriptionEventHandler(makeCtx(db), {2123      projectId: PROJECT_ID as never,2124      eventId: oldStartedId as never,2125    });2126    const replacementStartedId = await db.insert("webhookEvents", {2127      projectId: PROJECT_ID,2128      type: "SubscriptionStarted",2129      source: "GooglePlayRealTimeDeveloperNotifications",2130      platform: "Android",2131      environment: "Production",2132      purchaseToken: "purchase_token_2",2133      productKind: "subscription",2134      productId: "premium_monthly",2135      subscriptionState: "Active",2136      sourceNotificationId: "google-replacement-started",2137      occurredAt: 2_000,2138      receivedAt: 2_000,2139    });2140    await applySubscriptionEventHandler(makeCtx(db), {2141      projectId: PROJECT_ID as never,2142      eventId: replacementStartedId as never,2143    });2144    const predecessorExpiredId = await db.insert("webhookEvents", {2145      projectId: PROJECT_ID,2146      type: "SubscriptionExpired",2147      source: "GooglePlayRealTimeDeveloperNotifications",2148      platform: "Android",2149      environment: "Production",2150      purchaseToken: TOKEN,2151      productKind: "subscription",2152      productId: "premium_monthly",2153      subscriptionState: "Expired",2154      sourceNotificationId: "google-predecessor-expired",2155      occurredAt: 3_000,2156      receivedAt: 3_000,2157    });2158    await applySubscriptionEventHandler(makeCtx(db), {2159      projectId: PROJECT_ID as never,2160      eventId: predecessorExpiredId as never,2161    });2162    const linkedId = await db.insert("webhookEvents", {2163      projectId: PROJECT_ID,2164      type: "SubscriptionProductChanged",2165      source: "GooglePlayRealTimeDeveloperNotifications",2166      platform: "Android",2167      environment: "Production",2168      purchaseToken: "purchase_token_2",2169      linkedPurchaseToken: TOKEN,2170      productKind: "subscription",2171      productId: "premium_yearly",2172      subscriptionState: "Active",2173      sourceNotificationId: "google-linked-replacement",2174      occurredAt: 4_000,2175      receivedAt: 4_000,2176    });2177 2178    await applySubscriptionEventHandler(makeCtx(db), {2179      projectId: PROJECT_ID as never,2180      eventId: linkedId as never,2181    });2182 2183    expect(db.rows("subscriptions")).toHaveLength(1);2184    expect(db.rows("subscriptions")[0]).toMatchObject({2185      purchaseToken: "purchase_token_2",2186      productId: "premium_yearly",2187      state: "Active",2188      lastEventId: linkedId,2189    });2190  });2191 2192  it("moves a multi-item Google replacement token without guessing a product", async () => {2193    const db = new MemDb();2194    const startedId = await seedWebhookEvent(db, {2195      type: "SubscriptionStarted",2196      notificationId: "google-old-token",2197      occurredAt: 1_000,2198    });2199    await applySubscriptionEventHandler(makeCtx(db), {2200      projectId: PROJECT_ID as never,2201      eventId: startedId as never,2202    });2203    const changedId = await db.insert("webhookEvents", {2204      projectId: PROJECT_ID,2205      type: "SubscriptionProductChanged",2206      source: "GooglePlayRealTimeDeveloperNotifications",2207      platform: "Android",2208      environment: "Production",2209      purchaseToken: "purchase_token_2",2210      linkedPurchaseToken: TOKEN,2211      productKind: "subscription",2212      sourceNotificationId: "google-bundle-change",2213      occurredAt: 2_000,2214      receivedAt: 2_000,2215    });2216 2217    await applySubscriptionEventHandler(makeCtx(db), {2218      projectId: PROJECT_ID as never,2219      eventId: changedId as never,2220    });2221 2222    expect(db.rows("subscriptions")).toHaveLength(1);2223    expect(db.rows("subscriptions")[0]).toMatchObject({2224      purchaseToken: "purchase_token_2",2225      productId: "premium_monthly",2226      lastEventId: changedId,2227    });2228    expect(db.rows("commerceEvents")).toHaveLength(1);2229  });2230});2231 2232describe("buildVerifiedSubscriptionSnapshot", () => {2233  it("bootstraps an active subscription from an entitled Google verification", () => {2234    const snapshot = buildVerifiedSubscriptionSnapshot({2235      platform: "Android",2236      productId: "premium_monthly",2237      purchaseState: HarmonizedPurchaseState.ENTITLED,2238      subscriptionState: "SUBSCRIPTION_STATE_ACTIVE",2239      expiresAt: 2_000_000_000_000,2240    });2241 2242    expect(snapshot).toEqual({2243      productId: "premium_monthly",2244      state: "Active",2245      expiresAt: 2_000_000_000_000,2246      renewsAt: undefined,2247      willRenew: true,2248      cancellationReason: undefined,2249      clearCancellationReason: true,2250      currency: undefined,2251      priceAmountMicros: undefined,2252    });2253  });2254 2255  it("treats pending-acknowledgment subscriptions as entitled while still bindable", () => {2256    const snapshot = buildVerifiedSubscriptionSnapshot({2257      platform: "Android",2258      productId: "premium_monthly",2259      purchaseState: HarmonizedPurchaseState.PENDING_ACKNOWLEDGMENT,2260      subscriptionState: "SUBSCRIPTION_STATE_ACTIVE",2261    });2262 2263    expect(snapshot).toMatchObject({2264      productId: "premium_monthly",2265      state: "Active",2266      willRenew: true,2267      clearCancellationReason: true,2268    });2269  });2270 2271  it("stamps Refunded only when Apple says the revocation was a refund", () => {2272    // revocationReason 1 is Apple's app-issue refund; 0 also covers Family2273    // Sharing loss, where asserting money moved back would be false.2274    const refunded = buildVerifiedSubscriptionSnapshot({2275      platform: "IOS",2276      productId: "premium_monthly",2277      purchaseState: HarmonizedPurchaseState.CANCELED,2278      revocationReasonIOS: 1,2279    });2280    expect(refunded).toMatchObject({2281      state: "Revoked",2282      willRenew: false,2283      cancellationReason: "Refunded",2284    });2285 2286    const familySharingLoss = buildVerifiedSubscriptionSnapshot({2287      platform: "IOS",2288      productId: "premium_monthly",2289      purchaseState: HarmonizedPurchaseState.CANCELED,2290      revocationReasonIOS: 0,2291    });2292    expect(familySharingLoss).toMatchObject({2293      state: "Revoked",2294      willRenew: false,2295    });2296    expect(familySharingLoss?.cancellationReason).toBeUndefined();2297 2298    const unknown = buildVerifiedSubscriptionSnapshot({2299      platform: "IOS",2300      productId: "premium_monthly",2301      purchaseState: HarmonizedPurchaseState.CANCELED,2302    });2303    expect(unknown?.cancellationReason).toBeUndefined();2304  });2305 2306  it("preserves access for canceled Google subscriptions until expiry", () => {2307    const snapshot = buildVerifiedSubscriptionSnapshot({2308      platform: "Android",2309      productId: "premium_monthly",2310      purchaseState: HarmonizedPurchaseState.CANCELED,2311      subscriptionState: "SUBSCRIPTION_STATE_CANCELED",2312      expiresAt: 2_000_000_000_000,2313    });2314 2315    expect(snapshot).toMatchObject({2316      productId: "premium_monthly",2317      state: "Active",2318      willRenew: false,2319      expiresAt: 2_000_000_000_000,2320    });2321    expect(snapshot?.cancellationReason).toBeUndefined();2322  });2323 2324  it("maps on-hold Google subscriptions to billing retry", () => {2325    const snapshot = buildVerifiedSubscriptionSnapshot({2326      platform: "Android",2327      productId: "premium_monthly",2328      purchaseState: HarmonizedPurchaseState.PENDING,2329      subscriptionState: "SUBSCRIPTION_STATE_ON_HOLD",2330    });2331 2332    expect(snapshot).toMatchObject({2333      productId: "premium_monthly",2334      state: "InBillingRetry",2335      cancellationReason: "BillingError",2336    });2337  });2338 2339  it("does not infer renewal status when Google omits subscriptionState", () => {2340    const snapshot = buildVerifiedSubscriptionSnapshot({2341      platform: "Android",2342      productId: "premium_monthly",2343      purchaseState: HarmonizedPurchaseState.PENDING_ACKNOWLEDGMENT,2344    });2345 2346    expect(snapshot).toMatchObject({2347      productId: "premium_monthly",2348      state: "Active",2349    });2350    expect(snapshot?.willRenew).toBeUndefined();2351    expect(snapshot?.cancellationReason).toBeUndefined();2352    expect(snapshot?.clearCancellationReason).toBeUndefined();2353  });2354 2355  it("does not create a subscription row for expected-product mismatches", () => {2356    const snapshot = buildVerifiedSubscriptionSnapshot({2357      platform: "Android",2358      productId: "premium_monthly",2359      purchaseState: HarmonizedPurchaseState.INAUTHENTIC,2360      subscriptionState: "SUBSCRIPTION_STATE_ACTIVE",2361    });2362 2363    expect(snapshot).toBeNull();2364  });2365 2366  it("does not create a subscription row when Google omits the product id", () => {2367    const snapshot = buildVerifiedSubscriptionSnapshot({2368      platform: "Android",2369      productId: "unknown",2370      purchaseState: HarmonizedPurchaseState.ENTITLED,2371      subscriptionState: "SUBSCRIPTION_STATE_ACTIVE",2372    });2373 2374    expect(snapshot).toBeNull();2375  });2376 2377  it("bootstraps an Apple subscription without guessing auto-renew status", () => {2378    const snapshot = buildVerifiedSubscriptionSnapshot({2379      platform: "IOS",2380      productId: "com.example.premium",2381      purchaseState: HarmonizedPurchaseState.ENTITLED,2382      expiresAt: 2_000_000_000_000,2383      currency: "USD",2384      priceAmountMicros: 9_990_000,2385    });2386 2387    expect(snapshot).toEqual({2388      productId: "com.example.premium",2389      state: "Active",2390      expiresAt: 2_000_000_000_000,2391      renewsAt: undefined,2392      cancellationReason: undefined,2393      clearCancellationReason: true,2394      currency: "USD",2395      priceAmountMicros: 9_990_000,2396    });2397  });2398});2399 2400describe("mergeVerifiedSubscriptionSnapshot", () => {2401  it("preserves existing timing and price fields when verify omits them", () => {2402    const snapshot = mergeVerifiedSubscriptionSnapshot(2403      {2404        expiresAt: 2_000_000_000_000,2405        renewsAt: 2_000_000_000_000,2406        willRenew: true,2407        cancellationReason: undefined,2408        currency: "USD",2409        priceAmountMicros: 9_990_000,2410      },2411      {2412        productId: "premium_monthly",2413        state: "Active",2414        willRenew: true,2415      },2416    );2417 2418    expect(snapshot).toEqual({2419      productId: "premium_monthly",2420      state: "Active",2421      expiresAt: 2_000_000_000_000,2422      renewsAt: 2_000_000_000_000,2423      willRenew: true,2424      cancellationReason: undefined,2425      currency: "USD",2426      priceAmountMicros: 9_990_000,2427    });2428  });2429 2430  it("clears stale cancellation reason when verified snapshots request it", () => {2431    const existing = {2432      expiresAt: undefined,2433      renewsAt: undefined,2434      willRenew: false,2435      cancellationReason: "UserCanceled" as const,2436      currency: undefined,2437      priceAmountMicros: undefined,2438    };2439    const appleSnapshot = buildVerifiedSubscriptionSnapshot({2440      platform: "IOS",2441      productId: "premium_monthly",2442      purchaseState: HarmonizedPurchaseState.ENTITLED,2443    });2444    const graceSnapshot = buildVerifiedSubscriptionSnapshot({2445      platform: "Android",2446      productId: "premium_monthly",2447      purchaseState: HarmonizedPurchaseState.ENTITLED,2448      subscriptionState: "SUBSCRIPTION_STATE_IN_GRACE_PERIOD",2449    });2450 2451    expect(appleSnapshot?.clearCancellationReason).toBe(true);2452    expect(graceSnapshot?.clearCancellationReason).toBe(true);2453 2454    const appleMerged = mergeVerifiedSubscriptionSnapshot(2455      existing,2456      appleSnapshot!,2457    );2458    const graceMerged = mergeVerifiedSubscriptionSnapshot(2459      existing,2460      graceSnapshot!,2461    );2462 2463    expect(appleMerged.cancellationReason).toBeUndefined();2464    expect(graceMerged.cancellationReason).toBeUndefined();2465    expect(graceMerged.willRenew).toBe(true);2466  });2467 2468  it("preserves cancellation reason when verify cannot prove auto-renew is enabled", () => {2469    const snapshot = mergeVerifiedSubscriptionSnapshot(2470      {2471        expiresAt: undefined,2472        renewsAt: undefined,2473        willRenew: false,2474        cancellationReason: "UserCanceled",2475        currency: undefined,2476        priceAmountMicros: undefined,2477      },2478      {2479        productId: "premium_monthly",2480        state: "Active",2481      },2482    );2483 2484    expect(snapshot.cancellationReason).toBe("UserCanceled");2485    expect(snapshot.willRenew).toBe(false);2486  });2487});2488 2489describe("recordVerifiedSubscriptionHandler", () => {2490  beforeEach(() => {2491    vi.setSystemTime(new Date("2026-01-01T00:00:00.000Z"));2492  });2493 2494  afterEach(() => {2495    vi.useRealTimers();2496  });2497 2498  it("rejects a receipt-verification write once project deletion starts", async () => {2499    const db = new MemDb();2500    await db.patch(PROJECT_ID, { pendingDeletion: true });2501 2502    await expect(2503      recordVerifiedSubscriptionHandler(makeCtx(db), {2504        projectId: PROJECT_ID as never,2505        platform: "Android",2506        purchaseToken: TOKEN,2507        productId: "premium_monthly",2508        purchaseState: HarmonizedPurchaseState.ENTITLED,2509      }),2510    ).rejects.toThrow("Project not found");2511    expect(db.rows("subscriptions")).toEqual([]);2512    expect(db.rows("subscriptionStats")).toEqual([]);2513  });2514 2515  it("creates a bindable subscription row from Google receipt verification", async () => {2516    const db = new MemDb();2517    db.seedProduct({2518      projectId: PROJECT_ID,2519      platform: "Android",2520      productId: "premium_monthly",2521      billingPeriod: "P1M",2522    });2523 2524    const subscriptionId = await recordVerifiedSubscriptionHandler(2525      makeCtx(db),2526      {2527        projectId: PROJECT_ID as never,2528        platform: "Android",2529        purchaseToken: TOKEN,2530        productId: "premium_monthly",2531        purchaseState: HarmonizedPurchaseState.ENTITLED,2532        subscriptionState: "SUBSCRIPTION_STATE_ACTIVE",2533        expiresAt: 1_769_904_000_000,2534        renewsAt: 1_769_904_000_000,2535        currency: "USD",2536        priceAmountMicros: 9_990_000,2537      },2538    );2539 2540    expect(subscriptionId).toBe("subscriptions_2");2541    expect(db.rows("subscriptions")).toMatchObject([2542      {2543        _id: "subscriptions_2",2544        projectId: PROJECT_ID,2545        purchaseToken: TOKEN,2546        productId: "premium_monthly",2547        platform: "Android",2548        state: "Active",2549        willRenew: true,2550        currency: "USD",2551        priceAmountMicros: 9_990_000,2552      },2553    ]);2554    expect(db.rows("subscriptionStats")).toMatchObject([2555      {2556        projectId: PROJECT_ID,2557        currency: "USD",2558        activeSubs: 1,2559        inGracePeriod: 0,2560        inBillingRetry: 0,2561        mrrMicros: 9_990_000,2562      },2563    ]);2564  });2565 2566  it("keeps a verified Google prepaid subscription non-renewing", async () => {2567    const db = new MemDb();2568 2569    await recordVerifiedSubscriptionHandler(makeCtx(db), {2570      projectId: PROJECT_ID as never,2571      platform: "Android",2572      purchaseToken: "prepaid-token",2573      productId: "prepaid_monthly",2574      purchaseState: HarmonizedPurchaseState.ENTITLED,2575      subscriptionState: "SUBSCRIPTION_STATE_ACTIVE",2576      expiresAt: 1_769_904_000_000,2577      willRenew: false,2578    });2579 2580    expect(db.rows("subscriptions")).toMatchObject([2581      {2582        purchaseToken: "prepaid-token",2583        state: "Active",2584        expiresAt: 1_769_904_000_000,2585        willRenew: false,2586        renewsAt: undefined,2587      },2588    ]);2589  });2590 2591  it("clears a prior renewal date when verification becomes non-renewing", async () => {2592    const db = new MemDb();2593    const input = {2594      projectId: PROJECT_ID as never,2595      platform: "Android" as const,2596      purchaseToken: "prepaid-transition-token",2597      productId: "premium_monthly",2598      purchaseState: HarmonizedPurchaseState.ENTITLED,2599      subscriptionState: "SUBSCRIPTION_STATE_ACTIVE",2600      expiresAt: 1_769_904_000_000,2601    };2602 2603    await recordVerifiedSubscriptionHandler(makeCtx(db), {2604      ...input,2605      renewsAt: 1_769_904_000_000,2606      willRenew: true,2607    });2608    await recordVerifiedSubscriptionHandler(makeCtx(db), {2609      ...input,2610      willRenew: false,2611    });2612 2613    expect(db.rows("subscriptions")).toMatchObject([2614      {2615        purchaseToken: "prepaid-transition-token",2616        state: "Active",2617        willRenew: false,2618        renewsAt: undefined,2619      },2620    ]);2621    expect(db.rows("subscriptions")).toHaveLength(1);2622  });2623 2624  it("creates an active bindable row for pending-acknowledgment Google subscriptions", async () => {2625    const db = new MemDb();2626    db.seedProduct({2627      projectId: PROJECT_ID,2628      platform: "Android",2629      productId: "premium_monthly",2630      billingPeriod: "P1M",2631    });2632 2633    await recordVerifiedSubscriptionHandler(makeCtx(db), {2634      projectId: PROJECT_ID as never,2635      platform: "Android",2636      purchaseToken: TOKEN,2637      productId: "premium_monthly",2638      purchaseState: HarmonizedPurchaseState.PENDING_ACKNOWLEDGMENT,2639      subscriptionState: "SUBSCRIPTION_STATE_ACTIVE",2640      expiresAt: 1_769_904_000_000,2641      currency: "USD",2642      priceAmountMicros: 9_990_000,2643    });2644 2645    expect(db.rows("subscriptions")).toMatchObject([2646      {2647        purchaseToken: TOKEN,2648        productId: "premium_monthly",2649        platform: "Android",2650        state: "Active",2651        willRenew: true,2652      },2653    ]);2654    expect(db.rows("subscriptionStats")).toMatchObject([2655      {2656        activeSubs: 1,2657        mrrMicros: 9_990_000,2658      },2659    ]);2660  });2661 2662  it("keeps repeated verification idempotent for subscription stats", async () => {2663    const db = new MemDb();2664    db.seedProduct({2665      projectId: PROJECT_ID,2666      platform: "Android",2667      productId: "premium_monthly",2668      billingPeriod: "P1M",2669    });2670    const args = {2671      projectId: PROJECT_ID as never,2672      platform: "Android" as const,2673      purchaseToken: TOKEN,2674      productId: "premium_monthly",2675      purchaseState: HarmonizedPurchaseState.ENTITLED,2676      subscriptionState: "SUBSCRIPTION_STATE_ACTIVE",2677      expiresAt: 1_769_904_000_000,2678      currency: "USD",2679      priceAmountMicros: 9_990_000,2680    };2681 2682    await recordVerifiedSubscriptionHandler(makeCtx(db), args);2683    await recordVerifiedSubscriptionHandler(makeCtx(db), args);2684 2685    expect(db.rows("subscriptions")).toHaveLength(1);2686    expect(db.rows("subscriptionStats")).toMatchObject([2687      {2688        activeSubs: 1,2689        mrrMicros: 9_990_000,2690      },2691    ]);2692  });2693 2694  it("does not let an older verification overwrite webhook-governed state", async () => {2695    const db = new MemDb();2696    const startedId = await seedWebhookEvent(db, {2697      type: "SubscriptionStarted",2698      notificationId: "apple-monthly",2699      occurredAt: 1_000,2700      platform: "IOS",2701      productId: "premium_monthly",2702    });2703    await applySubscriptionEventHandler(makeCtx(db), {2704      projectId: PROJECT_ID as never,2705      eventId: startedId as never,2706    });2707    const renewedId = await seedWebhookEvent(db, {2708      type: "SubscriptionRenewed",2709      notificationId: "apple-yearly",2710      occurredAt: 2_000,2711      platform: "IOS",2712      productId: "premium_yearly",2713    });2714    await db.patch(renewedId, { expiresAt: 1_900_000_000_000 });2715    await applySubscriptionEventHandler(makeCtx(db), {2716      projectId: PROJECT_ID as never,2717      eventId: renewedId as never,2718    });2719 2720    await recordVerifiedSubscriptionHandler(makeCtx(db), {2721      projectId: PROJECT_ID as never,2722      platform: "IOS",2723      purchaseToken: TOKEN,2724      productId: "premium_monthly",2725      purchaseState: HarmonizedPurchaseState.ENTITLED,2726      expiresAt: 1_700_000_000_000,2727    });2728    expect(db.rows("subscriptions")[0]).toMatchObject({2729      productId: "premium_yearly",2730      expiresAt: 1_900_000_000_000,2731      lastEventSourceNotificationId: "apple-yearly",2732    });2733  });2734 2735  it("creates a bindable Apple subscription row from receipt verification", async () => {2736    const db = new MemDb();2737    db.seedProduct({2738      projectId: PROJECT_ID,2739      platform: "IOS",2740      productId: "com.example.premium",2741      billingPeriod: "P1M",2742    });2743 2744    const subscriptionId = await recordVerifiedSubscriptionHandler(2745      makeCtx(db),2746      {2747        projectId: PROJECT_ID as never,2748        platform: "IOS",2749        purchaseToken: "original_transaction_1",2750        productId: "com.example.premium",2751        purchaseState: HarmonizedPurchaseState.ENTITLED,2752        expiresAt: 1_769_904_000_000,2753        currency: "USD",2754        priceAmountMicros: 9_990_000,2755      },2756    );2757 2758    expect(subscriptionId).toBe("subscriptions_2");2759    expect(db.rows("subscriptions")).toMatchObject([2760      {2761        _id: "subscriptions_2",2762        projectId: PROJECT_ID,2763        purchaseToken: "original_transaction_1",2764        productId: "com.example.premium",2765        platform: "IOS",2766        state: "Active",2767        willRenew: undefined,2768      },2769    ]);2770    expect(db.rows("subscriptionStats")).toMatchObject([2771      {2772        projectId: PROJECT_ID,2773        currency: "USD",2774        activeSubs: 1,2775        mrrMicros: 9_990_000,2776      },2777    ]);2778  });2779 2780  it("supports the verify -> bind flow for SDK clients", async () => {2781    const db = new MemDb();2782    db.seedProduct({2783      projectId: PROJECT_ID,2784      platform: "Android",2785      productId: "premium_monthly",2786      billingPeriod: "P1M",2787    });2788 2789    const subscriptionId = await recordVerifiedSubscriptionHandler(2790      makeCtx(db),2791      {2792        projectId: PROJECT_ID as never,2793        platform: "Android",2794        purchaseToken: TOKEN,2795        productId: "premium_monthly",2796        purchaseState: HarmonizedPurchaseState.ENTITLED,2797        subscriptionState: "SUBSCRIPTION_STATE_ACTIVE",2798        expiresAt: 1_769_904_000_000,2799        renewsAt: 1_769_904_000_000,2800        currency: "USD",2801        priceAmountMicros: 9_990_000,2802      },2803    );2804 2805    const boundId = await bindSubscriptionToUserHandler(makeCtx(db), {2806      projectId: PROJECT_ID as never,2807      purchaseToken: TOKEN,2808      userId: "user-1",2809    });2810 2811    expect(boundId).toBe(subscriptionId);2812    expect(db.rows("subscriptions")).toMatchObject([2813      {2814        _id: subscriptionId,2815        projectId: PROJECT_ID,2816        purchaseToken: TOKEN,2817        productId: "premium_monthly",2818        platform: "Android",2819        state: "Active",2820        userId: "user-1",2821        willRenew: true,2822        currency: "USD",2823        priceAmountMicros: 9_990_000,2824      },2825    ]);2826 2827    const eventId = await seedWebhookEvent(db, {2828      type: "SubscriptionStarted",2829      notificationId: "message-after-bind",2830      occurredAt: 2_000,2831    });2832    await applySubscriptionEventHandler(makeCtx(db), {2833      projectId: PROJECT_ID as never,2834      eventId: eventId as never,2835    });2836    expect(db.rows("commerceEvents")).toMatchObject([2837      { eventType: "subscription.started", userId: "user-1" },2838      { eventType: "entitlement.granted", userId: "user-1" },2839    ]);2840  });2841 2842  it("rejects unbounded user ids at the Convex storage boundary", async () => {2843    const db = new MemDb();2844 2845    for (const userId of ["   ", "u".repeat(257)]) {2846      await expect(2847        bindSubscriptionToUserHandler(makeCtx(db), {2848          projectId: PROJECT_ID as never,2849          purchaseToken: TOKEN,2850          userId,2851        }),2852      ).rejects.toThrow("userId must be nonblank and at most 256 characters");2853    }2854    expect(db.rows("subscriptions")).toHaveLength(0);2855    expect(db.rows("commerceEvents")).toHaveLength(0);2856  });2857 2858  it("emits a correlated grant when the webhook arrives before binding", async () => {2859    const db = new MemDb();2860    db.seedProduct({2861      projectId: PROJECT_ID,2862      platform: "Android",2863      productId: "premium_monthly",2864      billingPeriod: "P1M",2865    });2866    const eventId = await seedWebhookEvent(db, {2867      type: "SubscriptionStarted",2868      notificationId: "message-before-bind",2869      occurredAt: 2_000,2870    });2871    await applySubscriptionEventHandler(makeCtx(db), {2872      projectId: PROJECT_ID as never,2873      eventId: eventId as never,2874    });2875 2876    await bindSubscriptionToUserHandler(makeCtx(db), {2877      projectId: PROJECT_ID as never,2878      purchaseToken: TOKEN,2879      userId: "user-1",2880    });2881 2882    expect(db.rows("commerceEvents").at(-1)).toMatchObject({2883      eventType: "entitlement.granted",2884      userId: "user-1",2885    });2886  });2887 2888  it("does not replay an unbound grant that expired before binding", async () => {2889    const db = new MemDb();2890    db.seedProduct({2891      projectId: PROJECT_ID,2892      platform: "Android",2893      productId: "premium_monthly",2894      billingPeriod: "P1M",2895    });2896    const started = await seedWebhookEvent(db, {2897      type: "SubscriptionStarted",2898      notificationId: "unbound-started",2899      occurredAt: 1_000,2900    });2901    await applySubscriptionEventHandler(makeCtx(db), {2902      projectId: PROJECT_ID as never,2903      eventId: started as never,2904    });2905    const expired = await seedWebhookEvent(db, {2906      type: "SubscriptionExpired",2907      notificationId: "unbound-expired",2908      occurredAt: 2_000,2909    });2910    await applySubscriptionEventHandler(makeCtx(db), {2911      projectId: PROJECT_ID as never,2912      eventId: expired as never,2913    });2914 2915    const beforeBind = db.rows("commerceEvents").map((row) => row.eventType);2916    expect(beforeBind).toEqual([2917      "subscription.started",2918      "subscription.expired",2919    ]);2920 2921    await bindSubscriptionToUserHandler(makeCtx(db), {2922      projectId: PROJECT_ID as never,2923      purchaseToken: TOKEN,2924      userId: "user-after-expiry",2925    });2926 2927    expect(db.rows("commerceEvents").map((row) => row.eventType)).toEqual(2928      beforeBind,2929    );2930    expect(db.rows("subscriptions")[0]).toMatchObject({2931      state: "Expired",2932      userId: "user-after-expiry",2933    });2934  });2935 2936  it("uses the compact source snapshot after the webhook row is pruned", async () => {2937    const db = new MemDb();2938    const eventId = await seedWebhookEvent(db, {2939      type: "SubscriptionStarted",2940      notificationId: "old-webhook",2941      occurredAt: 2_000,2942    });2943    await applySubscriptionEventHandler(makeCtx(db), {2944      projectId: PROJECT_ID as never,2945      eventId: eventId as never,2946    });2947    await db.delete(eventId);2948 2949    await bindSubscriptionToUserHandler(makeCtx(db), {2950      projectId: PROJECT_ID as never,2951      purchaseToken: TOKEN,2952      userId: "user-after-retention",2953    });2954    expect(db.rows("commerceEvents").at(-1)).toMatchObject({2955      eventType: "entitlement.granted",2956      productId: "premium_monthly",2957      userId: "user-after-retention",2958      sourceStoreNotificationId: "old-webhook",2959    });2960  });2961});2962 2963describe("user binding authorization", () => {2964  const seedBound = async (db: MemDb, userId?: string) => {2965    db.seedProduct({2966      projectId: PROJECT_ID,2967      platform: "Android",2968      productId: "premium_monthly",2969      billingPeriod: "P1M",2970    });2971    await db.insert("subscriptions", {2972      projectId: PROJECT_ID,2973      platform: "Android",2974      purchaseToken: TOKEN,2975      productId: "premium_monthly",2976      state: "Active",2977      willRenew: true,2978      updatedAt: 0,2979      ...(userId ? { userId } : {}),2980    });2981  };2982 2983  // Token possession is not proof of ownership, and a distinct rejection would2984  // tell any holder of the app-embedded publishable key that a token exists2985  // and belongs to someone.2986  it("reports an already-bound subscription the same as an unknown token", async () => {2987    const owned = new MemDb();2988    await seedBound(owned, "victim");2989    const unknown = new MemDb();2990    await seedBound(unknown, "victim");2991    await unknown.delete(unknown.rows("subscriptions")[0]._id);2992 2993    const attacker = {2994      projectId: PROJECT_ID as never,2995      purchaseToken: TOKEN,2996      userId: "attacker",2997    };2998    await expect(2999      bindSubscriptionToUserHandler(makeCtx(owned) as never, attacker),3000    ).resolves.toBeNull();3001    await expect(3002      bindSubscriptionToUserHandler(makeCtx(unknown) as never, attacker),3003    ).resolves.toBeNull();3004    expect(owned.rows("subscriptions")[0].userId).toBe("victim");3005  });3006 3007  // Erasure unlinked the previous owner. Once the job is gone the record is3008  // unowned, and an operator may associate it again exactly as a purchase can.3009  it("rebinds a record whose erased owner has already been unlinked", async () => {3010    const db = new MemDb();3011    await seedBound(db, undefined);3012    await db.patch(db.rows("subscriptions")[0]._id, { accountErased: true });3013    expect(3014      await rebindSubscriptionToUserHandler(makeCtx(db), {3015        projectId: PROJECT_ID as never,3016        purchaseToken: TOKEN,3017        userId: "real-owner",3018      }),3019    ).not.toBeNull();3020    const row = db.rows("subscriptions")[0];3021    expect(row.userId).toBe("real-owner");3022    expect(row.accountErased).toBeUndefined();3023  });3024 3025  it("still refuses to move a record away from its live owner", async () => {3026    const db = new MemDb();3027    await seedBound(db, "real-owner");3028    expect(3029      await bindSubscriptionToUserHandler(makeCtx(db), {3030        projectId: PROJECT_ID as never,3031        purchaseToken: TOKEN,3032        userId: "someone-else",3033      }),3034    ).toBeNull();3035    expect(db.rows("subscriptions")[0].userId).toBe("real-owner");3036  });3037 3038  it.each(["queued", "running", "completed"])(3039    "refuses rebind into or out of an account with a %s erasure job",3040    async (status) => {3041      for (const erasedUser of ["source-user", "target-user"]) {3042        const db = new MemDb();3043        await seedBound(db, "source-user");3044        const hashKey = "erasure-test-key";3045        await db.patch(PROJECT_ID, { userErasureHashKey: hashKey });3046        await db.insert("subscriptionUserErasureJobs", {3047          projectId: PROJECT_ID,3048          userIdHash: await hmacSha256Hex(hashKey, erasedUser),3049          status,3050        });3051        const before = structuredClone(db.rows("subscriptions"));3052        expect(3053          await rebindSubscriptionToUserHandler(makeCtx(db), {3054            projectId: PROJECT_ID as never,3055            purchaseToken: TOKEN,3056            userId: "target-user",3057          }),3058        ).toBeNull();3059        expect(db.rows("subscriptions")).toEqual(before);3060        expect(db.rows("commerceEvents")).toEqual([]);3061      }3062    },3063  );3064 3065  // A consumer gating access on commerce events must be told the purchase3066  // moved, or the wrong user keeps access and the real one never gets it.3067  it("revokes the old user and grants the new one on a rebind", async () => {3068    const db = new MemDb();3069    db.seedProduct({3070      projectId: PROJECT_ID,3071      platform: "Android",3072      productId: "premium_monthly",3073      billingPeriod: "P1M",3074    });3075    const priorId = await seedWebhookEvent(db, {3076      type: "SubscriptionStarted",3077      notificationId: "rebind-source",3078      occurredAt: 1_000,3079    });3080    await db.insert("subscriptions", {3081      projectId: PROJECT_ID,3082      platform: "Android",3083      purchaseToken: TOKEN,3084      productId: "premium_monthly",3085      state: "Active",3086      willRenew: true,3087      userId: "wrong-user",3088      lastEventId: priorId,3089      lastEventOccurredAt: 1_000,3090      lastEventSourceNotificationId: "rebind-source",3091      lastEventSource: {3092        type: "SubscriptionStarted",3093        environment: "Production",3094        productId: "premium_monthly",3095      },3096      expiresAt: Date.now() + 86_400_000,3097      updatedAt: 0,3098    });3099 3100    await rebindSubscriptionToUserHandler(makeCtx(db), {3101      projectId: PROJECT_ID as never,3102      purchaseToken: TOKEN,3103      userId: "real-owner",3104    });3105 3106    const emitted = db3107      .rows("commerceEvents")3108      .map((r) => [r.eventType, r.userId]);3109    expect(emitted).toEqual([3110      ["entitlement.revoked", "wrong-user"],3111      ["entitlement.granted", "real-owner"],3112    ]);3113  });3114 3115  // A rebind that cannot attribute events leaves the developer backend3116  // believing the old user still owns the purchase. Say so rather than3117  // reporting plain success.3118  it("reports a rebind it could not notify about", async () => {3119    const db = new MemDb();3120    db.seedProduct({3121      projectId: PROJECT_ID,3122      platform: "Android",3123      productId: "premium_monthly",3124      billingPeriod: "P1M",3125    });3126    await db.insert("subscriptions", {3127      projectId: PROJECT_ID,3128      platform: "Android",3129      purchaseToken: TOKEN,3130      productId: "premium_monthly",3131      state: "Active",3132      willRenew: true,3133      userId: "wrong-user",3134      expiresAt: Date.now() + 86_400_000,3135      updatedAt: 0,3136    });3137 3138    const outcome = await rebindSubscriptionToUserHandler(makeCtx(db), {3139      projectId: PROJECT_ID as never,3140      purchaseToken: TOKEN,3141      userId: "real-owner",3142    });3143    expect(outcome?.notified).toBe(false);3144    expect(db.rows("subscriptions")[0].userId).toBe("real-owner");3145    expect(db.rows("commerceEvents")).toEqual([]);3146  });3147 3148  it("lets an operator move a wrong binding", async () => {3149    const db = new MemDb();3150    await seedBound(db, "wrong-user");3151    await expect(3152      rebindSubscriptionToUserHandler(makeCtx(db) as never, {3153        projectId: PROJECT_ID as never,3154        purchaseToken: TOKEN,3155        userId: "real-owner",3156      }),3157    ).resolves.not.toBeNull();3158    expect(db.rows("subscriptions")[0].userId).toBe("real-owner");3159  });3160});3161