CI (SIAX Cloud) / security (push) Successful in 14s
Security Scan (SIAX Cloud) / security (push) Successful in 21s
CI (SIAX Cloud) / security (pull_request) Successful in 15s
Security Scan (SIAX Cloud) / security (pull_request) Successful in 15s
CI (SIAX Cloud) / quality (push) Successful in 44s
CI (SIAX Cloud) / integration (push) Successful in 45s
CI (SIAX Cloud) / integration (pull_request) Successful in 27s
CI (SIAX Cloud) / quality (pull_request) Successful in 1m45s
The `resolveStoreMode` kill switch (Master-DoD §3/§10) covered the newer `src/repositories/*` stores but not two older paths, both of which are live production execution paths today. 1. LEGACY NEXT.JS STORES. `lib/orders-store.ts` and `lib/subscriptions-store.ts` predate `src/platform/dependency-mode.ts` and called `getSql()` directly, silently falling back to module-global `Map`s when no database is configured. For orders, payments and subscriptions that is not a degraded mode, it is invisible loss of money-bearing state: the API returns 201 and the row evaporates on the next restart or on any other replica. The live 0pt deployment (Coolify app 54) runs NODE_ENV=production with DATABASE_URL, POSTGRES_URL and POSTGRES_PRISMA_URL all unset — so this is the behavior in production right now, across 7 API routes. These now go through `getSqlForDurableStore()`, which reuses the SAME `resolveStoreMode` decision every `src/repositories/*` store already uses rather than re-implementing a second production check. The reuse is load-bearing: `isProductionRuntime` deliberately exempts `NEXT_PHASE=phase-production-build`, because `next build` runs page-data collection under NODE_ENV=production while serving nothing. A naive `process.env.NODE_ENV === "production"` test here would have failed the BUILD instead of protecting a request path. 2. LOCALBLOBSTORE. It writes document bytes to a container-local disk while `document-capture-service` registers an artifact pointer claiming the bytes are stored — durable object storage silently swapped for ephemeral disk. It is the default blob store for both the capture and extraction services. In production it now raises DEPENDENCY_UNAVAILABLE (capability blocked: no ST0RE bytes path exists yet) instead of fabricating success. Asserted per-operation, not in the constructor, so merely wiring the services is not fatal — only actually persisting or reading bytes is. Evidence: full suite 956 passed / 184 skipped, typecheck clean, dependency-cruiser 0 errors, lint identical to main's pre-existing baseline (17 problems, none in changed files). Both guards asserted in both directions and mutation-tested: bypassing resolveStoreMode -> 2 failed; dropping the write assert -> 2 failed; dropping the read assert -> 1 failed; all restored -> 8 passed. 7 non-test call sites. DEPLOYMENT NOTE: merging changes no runtime (no Gitea webhook exists on any repo; deploys are manual). Deploying this build REQUIRES provisioning DATABASE_URL for 0pt first, or the order/subscription routes will correctly return DEPENDENCY_UNAVAILABLE instead of silently accepting orders into memory. That provisioning is a Simon action. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
37 lines
1.4 KiB
TypeScript
37 lines
1.4 KiB
TypeScript
import { getSqlForDurableStore } from "@/lib/db";
|
|
|
|
export interface Subscription {
|
|
sub_id: string;
|
|
order_id: string;
|
|
cadence: string;
|
|
webhook_url: string | null;
|
|
status: string;
|
|
next_run_at: string | null;
|
|
created_at: string;
|
|
}
|
|
|
|
const mem = new Map<string, Subscription>();
|
|
|
|
export async function createSubscription(order_id: string, cadence: string, webhook_url: string | null): Promise<Subscription> {
|
|
const sql = getSqlForDurableStore("0pt.subscriptions");
|
|
if (sql) {
|
|
const [row] = await sql<Subscription[]>`
|
|
INSERT INTO subscriptions (order_id, cadence, webhook_url, status)
|
|
VALUES (${order_id}, ${cadence}, ${webhook_url}, 'active')
|
|
RETURNING sub_id, order_id, cadence, webhook_url, status, next_run_at, now() AS created_at`;
|
|
return row;
|
|
}
|
|
const id = globalThis.crypto?.randomUUID?.() ?? `sub-${Math.floor(Math.random() * 1e9)}`;
|
|
const sub: Subscription = { sub_id: id, order_id, cadence, webhook_url, status: "active", next_run_at: null, created_at: new Date().toISOString() };
|
|
mem.set(id, sub);
|
|
return sub;
|
|
}
|
|
|
|
export async function listSubscriptions(): Promise<Subscription[]> {
|
|
const sql = getSqlForDurableStore("0pt.subscriptions");
|
|
if (sql) {
|
|
return sql<Subscription[]>`SELECT sub_id, order_id, cadence, webhook_url, status, next_run_at, created_at FROM subscriptions ORDER BY created_at DESC LIMIT 100`;
|
|
}
|
|
return [...mem.values()].sort((a, b) => b.created_at.localeCompare(a.created_at));
|
|
}
|