import {
  IntegrationRuntimeError
} from "../chunk-H4XYLS7T.js";
import "../chunk-PZ5AY32C.js";

// src/stripe/errors.ts
var BillingError = class extends IntegrationRuntimeError {
  billingCode;
  context;
  constructor(input) {
    super({
      code: mapToIntegrationCode(input.code),
      message: input.message,
      status: statusForBillingCode(input.code),
      userAction: input.userAction ?? defaultUserAction(input.code),
      metadata: input.context
    });
    this.name = "BillingError";
    this.billingCode = input.code;
    this.context = input.context ?? {};
  }
};
var ConfigError = class extends BillingError {
  constructor(input) {
    super({
      code: "tenant_not_configured",
      message: input.message,
      context: input.context,
      userAction: { type: "contact_support", label: "Contact support" }
    });
    this.name = "ConfigError";
  }
};
function mapToIntegrationCode(code) {
  switch (code) {
    case "subscription_required":
    case "subscription_inactive":
    case "subscription_past_due":
    case "trial_expired":
    case "free_tier_exhausted":
      return "action_denied";
    case "tenant_not_configured":
      return "provider_error";
    case "webhook_secret_missing":
      return "provider_auth_failed";
    case "webhook_event_unknown":
    case "webhook_replay":
      return "input_invalid";
  }
}
function statusForBillingCode(code) {
  switch (code) {
    case "subscription_required":
    case "subscription_inactive":
    case "subscription_past_due":
    case "trial_expired":
    case "free_tier_exhausted":
      return 403;
    case "tenant_not_configured":
      return 500;
    case "webhook_secret_missing":
      return 401;
    case "webhook_event_unknown":
    case "webhook_replay":
      return 400;
  }
}
function defaultUserAction(code) {
  switch (code) {
    case "subscription_required":
      return { type: "change_request", label: "Subscribe to continue" };
    case "subscription_inactive":
    case "subscription_past_due":
      return { type: "change_request", label: "Update billing" };
    case "trial_expired":
      return { type: "change_request", label: "Choose a plan" };
    case "free_tier_exhausted":
      return { type: "change_request", label: "Upgrade for more usage" };
    case "tenant_not_configured":
    case "webhook_secret_missing":
      return { type: "contact_support", label: "Contact support" };
    case "webhook_event_unknown":
    case "webhook_replay":
      return void 0;
  }
}

// src/stripe/subscription-state.ts
var SUBSCRIPTION_STATES = Object.freeze([
  "incomplete",
  "incomplete_expired",
  "trialing",
  "active",
  "past_due",
  "canceled",
  "unpaid",
  "paused"
]);
var TRANSITIONS = Object.freeze({
  incomplete: /* @__PURE__ */ new Set(["active", "trialing", "incomplete_expired", "canceled"]),
  incomplete_expired: /* @__PURE__ */ new Set([]),
  trialing: /* @__PURE__ */ new Set(["active", "past_due", "canceled", "paused", "unpaid"]),
  active: /* @__PURE__ */ new Set(["past_due", "canceled", "paused", "unpaid", "trialing"]),
  past_due: /* @__PURE__ */ new Set(["active", "canceled", "unpaid", "paused"]),
  canceled: /* @__PURE__ */ new Set([]),
  unpaid: /* @__PURE__ */ new Set(["active", "canceled", "past_due"]),
  paused: /* @__PURE__ */ new Set(["active", "canceled", "past_due"])
});
function isValidTransition(from, to) {
  if (from === to) return true;
  return TRANSITIONS[from].has(to);
}
function applyTransition(current, next, options = {}) {
  if (!isValidTransition(current.state, next.state)) {
    throw new BillingError({
      code: "webhook_event_unknown",
      message: `Illegal subscription transition ${current.state} \u2192 ${next.state}`,
      context: {
        workspaceId: current.workspaceId,
        subscriptionId: current.subscriptionId,
        subscriptionState: current.state,
        eventId: options.eventId
      }
    });
  }
  const now = (options.now ?? Date.now)();
  return {
    ...current,
    ...next,
    version: current.version + 1,
    lastEventId: options.eventId ?? current.lastEventId,
    updatedAt: now
  };
}
function gateAccess(state) {
  switch (state) {
    case "active":
    case "trialing":
      return { allowed: true };
    case "past_due":
      return { allowed: true, warn: "past_due" };
    case "paused":
      return { allowed: false, reason: "subscription_past_due" };
    case "canceled":
    case "unpaid":
      return { allowed: false, reason: "subscription_inactive" };
    case "incomplete":
    case "incomplete_expired":
      return { allowed: false, reason: "subscription_inactive" };
  }
}
var InMemorySubscriptionStore = class {
  records = /* @__PURE__ */ new Map();
  async load(workspaceId) {
    const r = this.records.get(workspaceId);
    return r ? { ...r } : null;
  }
  async save(record) {
    this.records.set(record.workspaceId, { ...record });
  }
  async saveIfVersion(record, expectedVersion) {
    const current = this.records.get(record.workspaceId);
    if (current && current.version !== expectedVersion) return false;
    if (!current && expectedVersion !== 0) return false;
    this.records.set(record.workspaceId, { ...record });
    return true;
  }
};
var FileSystemSubscriptionStore = class {
  constructor(rootDir) {
    this.rootDir = rootDir;
  }
  rootDir;
  async load(workspaceId) {
    const fs = await import("fs/promises");
    const path = await import("path");
    const file = path.join(this.rootDir, this.fileName(workspaceId));
    try {
      const raw = await fs.readFile(file, "utf-8");
      return JSON.parse(raw);
    } catch (err) {
      if (isNodeENOENT(err)) return null;
      throw err;
    }
  }
  async save(record) {
    const fs = await import("fs/promises");
    const path = await import("path");
    await fs.mkdir(this.rootDir, { recursive: true });
    const file = path.join(this.rootDir, this.fileName(record.workspaceId));
    const tmp = `${file}.tmp-${process.pid}-${Date.now()}`;
    await fs.writeFile(tmp, JSON.stringify(record), "utf-8");
    await fs.rename(tmp, file);
  }
  async saveIfVersion(record, expectedVersion) {
    const existing = await this.load(record.workspaceId);
    if (existing && existing.version !== expectedVersion) return false;
    if (!existing && expectedVersion !== 0) return false;
    await this.save(record);
    return true;
  }
  /** Safe filename: workspaceId is restricted to a charset that maps 1:1
   *  to a posix filename. Anything outside is hex-encoded so we can never
   *  escape the rootDir via `../`. */
  fileName(workspaceId) {
    if (!/^[A-Za-z0-9_.-]+$/.test(workspaceId)) {
      return `${Buffer.from(workspaceId, "utf-8").toString("hex")}.json`;
    }
    return `${workspaceId}.json`;
  }
};
function isNodeENOENT(err) {
  return !!err && typeof err === "object" && err.code === "ENOENT";
}
function makeSubscriptionRecord(input) {
  const now = (input.now ?? Date.now)();
  return {
    workspaceId: input.workspaceId,
    customerId: input.customerId,
    subscriptionId: input.subscriptionId,
    state: input.state,
    priceId: input.priceId,
    currentPeriodEnd: input.currentPeriodEnd,
    trialEnd: input.trialEnd ?? null,
    cancelAtPeriodEnd: input.cancelAtPeriodEnd ?? false,
    version: 0,
    lastEventId: null,
    updatedAt: now
  };
}

// src/stripe/webhooks.ts
var StripeBillingDispatcher = class {
  store;
  resolveWorkspaceId;
  listener;
  onError;
  now;
  maxCasRetries;
  constructor(opts) {
    this.store = opts.store;
    this.resolveWorkspaceId = opts.resolveWorkspaceId ?? defaultResolveWorkspaceId;
    this.listener = opts.listener;
    this.onError = opts.onError ?? defaultOnError;
    this.now = opts.now ?? Date.now;
    this.maxCasRetries = opts.maxCasRetries ?? 3;
  }
  /** Drive one envelope through the pipeline. Idempotent w.r.t. the
   *  event id (replays are a no-op + emit `event_replay`). */
  async dispatch(envelope) {
    const evt = envelope.payload;
    if (!evt || typeof evt !== "object" || typeof evt.id !== "string" || typeof evt.type !== "string") {
      this.onError(new Error("Stripe envelope missing id or type"), {
        eventId: "unknown",
        type: "unknown"
      });
      return;
    }
    try {
      await this.handle(evt);
    } catch (err) {
      this.onError(err, { eventId: evt.id, type: evt.type });
    }
  }
  async handle(evt) {
    switch (evt.type) {
      case "customer.subscription.created":
        return this.handleSubCreated(evt);
      case "customer.subscription.updated":
        return this.handleSubUpdated(evt);
      case "customer.subscription.deleted":
        return this.handleSubDeleted(evt);
      case "customer.subscription.trial_will_end":
        return this.handleTrialWillEnd(evt);
      case "customer.subscription.paused":
        return this.handleSubLifecycle(evt, "paused");
      case "customer.subscription.resumed":
        return this.handleSubLifecycle(evt, "active");
      case "invoice.paid":
        return this.handleInvoicePaid(evt);
      case "invoice.payment_failed":
        return this.handleInvoicePaymentFailed(evt);
      default:
        await this.emit({ kind: "event_unhandled", eventId: evt.id, type: evt.type });
        return;
    }
  }
  /* --------------------- subscription event handlers ------------------- */
  async handleSubCreated(evt) {
    const sub = evt.data.object;
    const workspaceId = await this.resolveWorkspaceId({
      customerId: sub.customer,
      subscriptionMetadata: sub.metadata
    });
    if (!workspaceId) return this.emitNoWorkspace(evt);
    const existing = await this.store.load(workspaceId);
    if (existing && existing.lastEventId === evt.id) {
      return this.emit({ kind: "event_replay", eventId: evt.id, type: evt.type });
    }
    if (existing && !canApplyFreshCreate(existing.state)) {
      return this.emit({
        kind: "event_dropped_out_of_order",
        eventId: evt.id,
        type: evt.type,
        reason: `existing state ${existing.state} cannot accept a fresh 'created'`
      });
    }
    const record = makeSubscriptionRecord({
      workspaceId,
      customerId: sub.customer,
      subscriptionId: sub.id,
      state: parseState(sub.status, evt.id),
      priceId: extractPriceId(sub),
      currentPeriodEnd: sub.current_period_end ?? null,
      trialEnd: sub.trial_end ?? null,
      cancelAtPeriodEnd: sub.cancel_at_period_end ?? false,
      now: this.now
    });
    const stamped = { ...record, lastEventId: evt.id };
    const expectedVersion = existing?.version ?? 0;
    const written = await this.cas(stamped, expectedVersion);
    if (!written) return;
    await this.emit({ kind: "subscription.created", eventId: evt.id, record: stamped });
  }
  async handleSubUpdated(evt) {
    const sub = evt.data.object;
    const workspaceId = await this.resolveWorkspaceId({
      customerId: sub.customer,
      subscriptionMetadata: sub.metadata
    });
    if (!workspaceId) return this.emitNoWorkspace(evt);
    const nextState = parseState(sub.status, evt.id);
    await this.advance(evt, workspaceId, (current) => {
      if (current.lastEventId === evt.id) return "replay";
      if (!isValidTransition(current.state, nextState)) return "out_of_order";
      const next = applyTransition(
        current,
        {
          state: nextState,
          priceId: extractPriceId(sub) ?? current.priceId,
          currentPeriodEnd: sub.current_period_end ?? current.currentPeriodEnd,
          trialEnd: sub.trial_end ?? current.trialEnd,
          cancelAtPeriodEnd: sub.cancel_at_period_end ?? current.cancelAtPeriodEnd
        },
        { eventId: evt.id, now: this.now }
      );
      return {
        next,
        emit: { kind: "subscription.updated", eventId: evt.id, previousState: current.state, record: next }
      };
    });
  }
  async handleSubDeleted(evt) {
    const sub = evt.data.object;
    const workspaceId = await this.resolveWorkspaceId({
      customerId: sub.customer,
      subscriptionMetadata: sub.metadata
    });
    if (!workspaceId) return this.emitNoWorkspace(evt);
    await this.advance(evt, workspaceId, (current) => {
      if (current.lastEventId === evt.id) return "replay";
      if (current.state === "canceled") return "replay";
      const next = applyTransition(
        current,
        { state: "canceled", priceId: null, currentPeriodEnd: sub.current_period_end ?? current.currentPeriodEnd },
        { eventId: evt.id, now: this.now }
      );
      return {
        next,
        emit: { kind: "subscription.deleted", eventId: evt.id, record: next }
      };
    });
  }
  async handleTrialWillEnd(evt) {
    const sub = evt.data.object;
    const workspaceId = await this.resolveWorkspaceId({
      customerId: sub.customer,
      subscriptionMetadata: sub.metadata
    });
    if (!workspaceId) return this.emitNoWorkspace(evt);
    const current = await this.store.load(workspaceId);
    if (!current) return this.emitNoWorkspace(evt);
    if (current.lastEventId === evt.id) {
      return this.emit({ kind: "event_replay", eventId: evt.id, type: evt.type });
    }
    const next = {
      ...current,
      lastEventId: evt.id,
      trialEnd: sub.trial_end ?? current.trialEnd,
      version: current.version + 1,
      updatedAt: this.now()
    };
    const written = await this.cas(next, current.version);
    if (!written) return;
    await this.emit({
      kind: "subscription.trial_will_end",
      eventId: evt.id,
      record: next,
      trialEndsAt: sub.trial_end ?? next.trialEnd ?? 0
    });
  }
  async handleSubLifecycle(evt, target) {
    const sub = evt.data.object;
    const workspaceId = await this.resolveWorkspaceId({
      customerId: sub.customer,
      subscriptionMetadata: sub.metadata
    });
    if (!workspaceId) return this.emitNoWorkspace(evt);
    await this.advance(evt, workspaceId, (current) => {
      if (current.lastEventId === evt.id) return "replay";
      if (!isValidTransition(current.state, target)) return "out_of_order";
      const next = applyTransition(current, { state: target }, { eventId: evt.id, now: this.now });
      const kind = target === "paused" ? "subscription.paused" : "subscription.resumed";
      return { next, emit: { kind, eventId: evt.id, record: next } };
    });
  }
  /* ----------------------- invoice event handlers ---------------------- */
  async handleInvoicePaid(evt) {
    const inv = evt.data.object;
    const workspaceId = await this.resolveWorkspaceId({
      customerId: inv.customer ?? "",
      invoiceMetadata: inv.metadata
    });
    let record = null;
    if (workspaceId) record = await this.store.load(workspaceId);
    await this.emit({
      kind: "invoice.paid",
      eventId: evt.id,
      record,
      invoiceId: inv.id,
      amountPaid: inv.amount_paid ?? 0
    });
  }
  async handleInvoicePaymentFailed(evt) {
    const inv = evt.data.object;
    const workspaceId = await this.resolveWorkspaceId({
      customerId: inv.customer ?? "",
      invoiceMetadata: inv.metadata
    });
    let record = null;
    if (workspaceId) record = await this.store.load(workspaceId);
    await this.emit({
      kind: "invoice.payment_failed",
      eventId: evt.id,
      record,
      invoiceId: inv.id,
      amountDue: inv.amount_due ?? 0
    });
  }
  /* ------------------------------- core -------------------------------- */
  /** Load, apply a transformation, CAS-write. The transformation may
   *  return 'replay' / 'out_of_order' for the dispatcher to emit
   *  diagnostic events instead. Retries on contention up to
   *  `maxCasRetries`; if exhausted, emits via `onError`. */
  async advance(evt, workspaceId, transform) {
    for (let attempt = 0; attempt < this.maxCasRetries; attempt++) {
      const current = await this.store.load(workspaceId);
      if (!current) return this.emitNoWorkspace(evt);
      const result = transform(current);
      if (result === "replay") {
        return this.emit({ kind: "event_replay", eventId: evt.id, type: evt.type });
      }
      if (result === "out_of_order") {
        return this.emit({
          kind: "event_dropped_out_of_order",
          eventId: evt.id,
          type: evt.type,
          reason: `current=${current.state}`
        });
      }
      const written = await this.store.saveIfVersion(result.next, current.version);
      if (written) return this.emit(result.emit);
    }
    this.onError(new BillingError({
      code: "webhook_event_unknown",
      message: `CAS contention exhausted after ${this.maxCasRetries} attempts`,
      context: { workspaceId, eventId: evt.id }
    }), { eventId: evt.id, type: evt.type });
  }
  async cas(record, expectedVersion) {
    for (let attempt = 0; attempt < this.maxCasRetries; attempt++) {
      const ok = await this.store.saveIfVersion(record, expectedVersion + attempt);
      if (ok) return true;
    }
    return false;
  }
  async emit(event) {
    if (!this.listener) return;
    try {
      await this.listener(event);
    } catch (err) {
      this.onError(err, {
        eventId: "eventId" in event ? event.eventId : "unknown",
        type: event.kind
      });
    }
  }
  emitNoWorkspace(evt) {
    return this.emit({
      kind: "event_dropped_out_of_order",
      eventId: evt.id,
      type: evt.type,
      reason: "workspaceId could not be resolved from event payload"
    });
  }
};
function combineListeners(...listeners) {
  return async (event) => {
    for (const l of listeners) await l(event);
  };
}
function defaultResolveWorkspaceId(input) {
  const sub = input.subscriptionMetadata?.workspaceId;
  if (sub) return sub;
  const inv = input.invoiceMetadata?.workspaceId;
  return inv ?? null;
}
function defaultOnError(err, context) {
  console.error("[StripeBillingDispatcher]", context, err);
}
function parseState(status, eventId) {
  switch (status) {
    case "incomplete":
    case "incomplete_expired":
    case "trialing":
    case "active":
    case "past_due":
    case "canceled":
    case "unpaid":
    case "paused":
      return status;
    default:
      throw new BillingError({
        code: "webhook_event_unknown",
        message: `Unknown Stripe subscription status '${status}'`,
        context: { eventId }
      });
  }
}
function canApplyFreshCreate(state) {
  return state === "incomplete" || state === "incomplete_expired";
}
function extractPriceId(sub) {
  return sub.items?.data?.[0]?.price?.id ?? null;
}

// src/stripe/pricing.ts
function findPlan(plans, id) {
  return plans.find((p) => p.id === id) ?? null;
}
function requirePlan(plans, id) {
  const plan = findPlan(plans, id);
  if (!plan) throw new Error(`pricing: unknown plan id '${id}'`);
  return plan;
}
async function createCheckoutUrl(client, input) {
  const priceId = input.plan.stripePriceIds[input.billing];
  if (!priceId) {
    throw new Error(`pricing: plan '${input.plan.id}' has no Stripe price for cadence '${input.billing}'`);
  }
  const successUrl = input.successUrl ?? client.config.successUrl;
  const cancelUrl = input.cancelUrl ?? client.config.cancelUrl;
  if (!successUrl || !cancelUrl) {
    throw new Error("pricing: successUrl and cancelUrl required (per-call or in TenantStripeConfig)");
  }
  const trialDays = input.trialDays ?? input.plan.trialDays;
  const body = {
    mode: "subscription",
    success_url: successUrl,
    cancel_url: cancelUrl,
    "line_items[0][price]": priceId,
    "line_items[0][quantity]": 1,
    "metadata[workspaceId]": input.workspaceId,
    "metadata[planId]": input.plan.id,
    "subscription_data[metadata][workspaceId]": input.workspaceId,
    "subscription_data[metadata][planId]": input.plan.id
  };
  if (input.customerId) body.customer = input.customerId;
  if (input.customerEmail && !input.customerId) body.customer_email = input.customerEmail;
  if (trialDays && trialDays > 0) {
    body["subscription_data[trial_period_days]"] = trialDays;
  }
  const extra = { ...input.plan.metadata ?? {}, ...input.metadata ?? {} };
  for (const [k, v] of Object.entries(extra)) {
    body[`metadata[${k}]`] = v;
    body[`subscription_data[metadata][${k}]`] = v;
  }
  const created = await client.mutate(
    "POST",
    "/checkout/sessions",
    body,
    input.idempotencyKey
  );
  if (!created.url) {
    throw new Error("pricing: Stripe checkout response missing url");
  }
  return { sessionId: created.id, url: created.url };
}
async function createBillingPortalUrl(client, input) {
  const created = await client.mutate(
    "POST",
    "/billing_portal/sessions",
    {
      customer: input.customerId,
      return_url: input.returnUrl
    },
    input.idempotencyKey
  );
  return { sessionId: created.id, url: created.url };
}

// src/stripe/tenant-config.ts
var PRODUCT_IDS = Object.freeze([
  "legal",
  "tax",
  "gtm",
  "creative",
  "agent-builder"
]);
var EnvTenantConfigResolver = class {
  constructor(env = process.env) {
    this.env = env;
  }
  env;
  resolve(productId) {
    const key = envKey(productId);
    const sk = this.env[`STRIPE_SK_${key}`];
    const wh = this.env[`STRIPE_WHSEC_${key}`];
    if (!sk || !wh) return null;
    return {
      productId,
      secretKey: sk,
      webhookSecret: wh,
      successUrl: this.env[`STRIPE_SUCCESS_URL_${key}`],
      cancelUrl: this.env[`STRIPE_CANCEL_URL_${key}`]
    };
  }
};
var StaticTenantConfigResolver = class {
  constructor(table) {
    this.table = table;
  }
  table;
  resolve(productId) {
    return this.table[productId] ?? null;
  }
};
function memoizeResolver(inner, ttlMs = 6e4) {
  const cache = /* @__PURE__ */ new Map();
  return {
    async resolve(productId) {
      const now = Date.now();
      const hit = cache.get(productId);
      if (hit && hit.expiresAt > now) return hit.config;
      const config = await inner.resolve(productId);
      cache.set(productId, { config, expiresAt: now + ttlMs });
      return config;
    }
  };
}
var STRIPE_API = "https://api.stripe.com/v1";
async function getStripeClient(productId, resolver) {
  const config = await resolver.resolve(productId);
  if (!config) {
    throw new ConfigError({
      message: `Stripe not configured for product '${productId}'. Set STRIPE_SK_${envKey(productId)} and STRIPE_WHSEC_${envKey(productId)}.`,
      context: { productId }
    });
  }
  return buildClient(config);
}
function buildStripeClient(config) {
  return buildClient(config);
}
function buildClient(config) {
  const auth = `Bearer ${config.secretKey}`;
  return {
    productId: config.productId,
    config,
    async get(path, query) {
      const qs = query ? `?${new URLSearchParams(query).toString()}` : "";
      const res = await fetch(`${STRIPE_API}${path}${qs}`, {
        headers: { authorization: auth },
        signal: AbortSignal.timeout(1e4)
      });
      if (!res.ok) {
        const text = await res.text().catch(() => "");
        throw new Error(`stripe ${path} ${res.status}: ${text.slice(0, 200)}`);
      }
      return await res.json();
    },
    async mutate(method, path, body, idempotencyKey) {
      const form = new URLSearchParams();
      for (const [k, v] of Object.entries(body)) {
        if (v === void 0) continue;
        form.set(k, String(v));
      }
      const init = {
        method,
        headers: {
          authorization: auth,
          "idempotency-key": idempotencyKey,
          ...method === "POST" ? { "content-type": "application/x-www-form-urlencoded" } : {}
        },
        signal: AbortSignal.timeout(15e3)
      };
      if (method === "POST") init.body = form;
      const res = await fetch(`${STRIPE_API}${path}`, init);
      if (!res.ok) {
        const text = await res.text().catch(() => "");
        throw new Error(`stripe ${path} ${res.status}: ${text.slice(0, 200)}`);
      }
      return await res.json();
    }
  };
}
function makeStripeSecretResolver(resolver) {
  return async function resolveSecret(providerId, headers) {
    if (providerId !== "stripe") return null;
    const productHeader = headers["x-tangle-product"];
    const productId = Array.isArray(productHeader) ? productHeader[0] : productHeader;
    if (!productId || !isProductId(productId)) return null;
    const config = await resolver.resolve(productId);
    return config?.webhookSecret ?? null;
  };
}
function isProductId(s) {
  return PRODUCT_IDS.includes(s);
}
function envKey(productId) {
  return productId.toUpperCase().replace(/-/g, "_");
}

// src/stripe/middleware.ts
async function requireActiveSubscription(input) {
  const record = await input.store.load(input.workspaceId);
  if (!record) {
    return {
      allowed: false,
      error: new BillingError({
        code: "subscription_required",
        message: "This workspace has no Stripe subscription.",
        context: { workspaceId: input.workspaceId }
      })
    };
  }
  const decision = gateAccess(record.state);
  if (!decision.allowed) {
    return {
      allowed: false,
      error: new BillingError({
        code: decision.reason === "subscription_inactive" ? "subscription_inactive" : decision.reason === "subscription_past_due" ? "subscription_past_due" : "subscription_required",
        message: `Subscription is ${record.state}.`,
        context: {
          workspaceId: input.workspaceId,
          subscriptionId: record.subscriptionId,
          subscriptionState: record.state
        }
      })
    };
  }
  if (decision.warn === "past_due" && input.denyPastDue) {
    return {
      allowed: false,
      error: new BillingError({
        code: "subscription_past_due",
        message: "Subscription is past due \u2014 this action requires a current payment method.",
        context: {
          workspaceId: input.workspaceId,
          subscriptionId: record.subscriptionId,
          subscriptionState: record.state
        }
      })
    };
  }
  let warn = decision.warn;
  if (!warn && record.state === "trialing" && record.trialEnd) {
    const TRIAL_WARN_SECONDS = 72 * 60 * 60;
    const nowSec = Math.floor(Date.now() / 1e3);
    if (record.trialEnd - nowSec < TRIAL_WARN_SECONDS) {
      warn = "trial_ending";
    }
  }
  return { allowed: true, record, warn };
}
async function withTrialAccess(input) {
  const createdAt = await input.trialStore.getCreatedAt(input.workspaceId);
  if (createdAt === null) {
    return { inTrial: false, daysRemaining: 0, trialEndsAt: null };
  }
  const now = (input.now ?? Date.now)();
  const trialEndsAt = createdAt + input.days * 24 * 60 * 60 * 1e3;
  const remainingMs = trialEndsAt - now;
  if (remainingMs <= 0) {
    return { inTrial: false, daysRemaining: 0, trialEndsAt };
  }
  const daysRemaining = Math.floor(remainingMs / (24 * 60 * 60 * 1e3));
  return { inTrial: true, daysRemaining, trialEndsAt };
}
async function getRemainingFreeTier(input) {
  const { used, total } = await input.freeTierStore.getUsage(input.workspaceId);
  const remaining = Math.max(0, total - used);
  return { remaining, total, exhausted: remaining === 0 };
}
async function gateSubscriptionOrTrial(input) {
  if (input.trialStore && input.trialDays) {
    const trial = await withTrialAccess({
      workspaceId: input.workspaceId,
      days: input.trialDays,
      trialStore: input.trialStore,
      now: input.now
    });
    if (trial.inTrial) {
      const trialRecord = trialSyntheticRecord(input.workspaceId, trial.trialEndsAt ?? 0);
      return { allowed: true, record: trialRecord, viaTrial: true, daysRemaining: trial.daysRemaining };
    }
  }
  return requireActiveSubscription({
    workspaceId: input.workspaceId,
    store: input.store,
    denyPastDue: input.denyPastDue
  });
}
function trialSyntheticRecord(workspaceId, trialEndsAt) {
  return {
    workspaceId,
    customerId: "",
    subscriptionId: "",
    state: "trialing",
    priceId: null,
    currentPeriodEnd: Math.floor(trialEndsAt / 1e3),
    trialEnd: Math.floor(trialEndsAt / 1e3),
    cancelAtPeriodEnd: false,
    version: 0,
    lastEventId: null,
    updatedAt: Date.now()
  };
}
export {
  BillingError,
  ConfigError,
  EnvTenantConfigResolver,
  FileSystemSubscriptionStore,
  InMemorySubscriptionStore,
  PRODUCT_IDS,
  SUBSCRIPTION_STATES,
  StaticTenantConfigResolver,
  StripeBillingDispatcher,
  applyTransition,
  buildStripeClient,
  combineListeners,
  createBillingPortalUrl,
  createCheckoutUrl,
  findPlan,
  gateAccess,
  gateSubscriptionOrTrial,
  getRemainingFreeTier,
  getStripeClient,
  isValidTransition,
  makeStripeSecretResolver,
  makeSubscriptionRecord,
  memoizeResolver,
  requireActiveSubscription,
  requirePlan,
  withTrialAccess
};
//# sourceMappingURL=index.js.map