{"version":3,"file":"config.mjs","names":[],"sources":["../../src/app-store/config.ts"],"sourcesContent":["import { invariant } from '@shware/utils';\nimport ms from 'ms';\nimport type { Metadata } from '../types';\n\nconst addMethod = Symbol('addMethod');\n\nclass Subscription<\n  NS extends Lowercase<string> = Lowercase<string>,\n  PE extends string = string,\n  BP extends string = string,\n  PL extends string = never,\n  PI extends string = never,\n  I extends string = never,\n> {\n  readonly plan: PE;\n  readonly billingPeriod: BP;\n  readonly config: AppStoreConfig<NS, PE, BP, PL, PI>;\n  readonly products: Map<string, Metadata>;\n\n  defaultProductId: I | null;\n\n  constructor(config: AppStoreConfig<NS, PE, BP, PL, PI>, plan: PE, billingPeriod: BP) {\n    this.plan = plan;\n    this.billingPeriod = billingPeriod;\n    this.config = config;\n    this.products = new Map();\n    this.defaultProductId = null;\n  }\n\n  /**\n   * Non-default products stay declared on purpose: they keep resolving for\n   * grandfathered subscribers and serve as price-experiment arms. Never\n   * sell a legacy product to new users unless that is intentional.\n   */\n  product = <K extends `${NS}.${Lowercase<string>}`>(\n    productId: K extends PI ? never : K,\n    metadata: Metadata = { credits: 0, expiresIn: '0' }\n  ): Subscription<NS, PE, BP, PL, PI | K, I | K> => {\n    invariant(!this.products.has(productId), `Duplicate product ${productId}`);\n    this.products.set(productId, metadata);\n    return this as Subscription<NS, PE, BP, PL, PI | K, I | K>;\n  };\n\n  default = (defaultProductId: I) => {\n    invariant(\n      this.products.has(defaultProductId),\n      `Default product ${defaultProductId} is not declared`\n    );\n    this.defaultProductId = defaultProductId;\n    this.config[addMethod](this as never);\n    return this.config;\n  };\n}\n\ntype Options = { appId: `${number}`; bundleId: string };\n\nexport class AppStoreConfig<\n  NS extends Lowercase<string> = Lowercase<string>,\n  PE extends string = string,\n  BP extends string = string,\n  PL extends string = never,\n  PI extends string = never,\n> {\n  readonly appId: string;\n  readonly bundleId: string;\n\n  private readonly products: Map<string, Metadata | null>;\n  private readonly consumables: Set<string>;\n  private readonly subscriptions: Map<string, Subscription<NS, PE, BP, PL, PI>>;\n  private readonly nonConsumables: Set<string>;\n\n  get productIds(): string[] {\n    return Array.from(this.products.keys());\n  }\n\n  // runtime mirror of the type-level guards: generic accumulation only\n  // protects a single fluent chain, not statement-style re-entry or casts\n  [addMethod] = (subscription: Subscription<NS, PE, BP, PL, PI>) => {\n    const key = `${subscription.plan}:${subscription.billingPeriod}`;\n    invariant(!this.subscriptions.has(key), `Duplicate plan ${key}`);\n    subscription.products.forEach((metadata, productId) => {\n      invariant(!this.products.has(productId), `Duplicate product ${productId}`);\n      this.products.set(productId, metadata);\n    });\n    this.subscriptions.set(key, subscription);\n    return this;\n  };\n\n  private constructor(options: Options) {\n    this.appId = options.appId;\n    this.bundleId = options.bundleId;\n    this.products = new Map();\n    this.consumables = new Set();\n    this.subscriptions = new Map();\n    this.nonConsumables = new Set();\n  }\n\n  static create = <NS extends Lowercase<string>, PE extends string, BP extends string = string>(\n    options: Options\n  ) => {\n    return new AppStoreConfig<NS, PE, BP>(options);\n  };\n\n  /** The same plan may be declared once per billing period; duplicate pairs are rejected. */\n  subscription = <K extends PE, P extends BP>(\n    plan: K,\n    billingPeriod: `${K}:${P}` extends PL ? never : P\n  ) => {\n    // oxlint-disable-next-line typescript/no-unnecessary-type-assertion -- tsc requires the cast (tsgolint false positive)\n    return new Subscription<NS, PE, BP, `${K}:${P}` | PL, PI>(this as never, plan, billingPeriod);\n  };\n\n  consumable = <K extends `${NS}.${Lowercase<string>}`>(\n    productId: K extends PI ? never : K,\n    metadata: Metadata = { credits: 0, expiresIn: '0' }\n  ) => {\n    invariant(!this.products.has(productId), `Duplicate product ${productId}`);\n    this.consumables.add(productId);\n    this.products.set(productId, metadata);\n    return this as AppStoreConfig<NS, PE, BP, PL, PI | K>;\n  };\n\n  nonConsumable = <K extends `${NS}.${Lowercase<string>}`>(productId: K extends PI ? never : K) => {\n    invariant(!this.products.has(productId), `Duplicate product ${productId}`);\n    this.nonConsumables.add(productId);\n    this.products.set(productId, null);\n    return this as AppStoreConfig<NS, PE, BP, PL, PI | K>;\n  };\n\n  /** Product id that new subscribers should purchase for the plan and billing period. */\n  getDefaultProductId = (plan: PE, billingPeriod: BP): string => {\n    const subscription = this.subscriptions.get(`${plan}:${billingPeriod}`);\n    invariant(subscription, `Subscription not found for ${plan}:${billingPeriod}`);\n    invariant(\n      subscription.defaultProductId,\n      `Default product not found for ${plan}:${billingPeriod}`\n    );\n    return subscription.defaultProductId;\n  };\n\n  getPlan = (productId: string): PE => {\n    for (const subscription of this.subscriptions.values()) {\n      if (subscription.products.has(productId)) return subscription.plan;\n    }\n    throw new Error(`Plan not found for product ${productId}`);\n  };\n\n  getBillingPeriod = (productId: string): BP => {\n    for (const subscription of this.subscriptions.values()) {\n      if (subscription.products.has(productId)) return subscription.billingPeriod;\n    }\n    throw new Error(`Billing period not found for product ${productId}`);\n  };\n\n  getMode = (productId: string): 'payment' | 'subscription' => {\n    if (this.consumables.has(productId) || this.nonConsumables.has(productId)) return 'payment';\n    for (const subscription of this.subscriptions.values()) {\n      if (subscription.products.has(productId)) return 'subscription';\n    }\n    throw new Error(`Mode not found for product ${productId}`);\n  };\n\n  getCreditAmount = (productId: string): number => {\n    const config = this.products.get(productId);\n    invariant(config !== undefined, `Product not found for ${productId}`);\n    invariant(config !== null, `Credits not available for non-consumable product ${productId}`);\n    return config.credits;\n  };\n\n  private readonly getCreditExpiresIn = (productId: string): number => {\n    const config = this.products.get(productId);\n    invariant(config, `Product not found for ${productId}`);\n    return ms(config.expiresIn);\n  };\n\n  getCreditExpiresAt = (productId: string): string => {\n    const expiresIn = this.getCreditExpiresIn(productId);\n    return new Date(Date.now() + expiresIn).toISOString();\n  };\n}\n"],"mappings":"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"}