fix(live): betalningsspärr i pilot + tydligt OCR-fel #453

Merged
admin merged 1 commits from fix/live-honesty-and-billing-guard into main 2026-11-02 19:14:58 +00:00
4 changed files with 48 additions and 10 deletions
+9
View File
@@ -27,6 +27,15 @@ export async function POST(request: NextRequest) {
if ('response' in verified) return verified.response;
const { tenantId } = verified.scope;
if (process.env.OPT_CREDITS_MODE === 'off') {
return NextResponse.json(
{
error: 'CREDITS_DISABLED',
message: 'Billing is not active yet (pilot mode). No payment can be taken.',
},
{ status: 503 },
);
}
const book = readCreditPriceBook();
if (!book) {
return NextResponse.json(
+1
View File
@@ -27,6 +27,7 @@ export async function GET(request: NextRequest) {
return NextResponse.json({
version: book.version,
currency: book.currency,
purchases_enabled: process.env.OPT_CREDITS_MODE !== 'off',
packs: book.packs.map((pack) => ({
id: pack.id,
credits: pack.credits,
+10 -1
View File
@@ -163,13 +163,22 @@ export async function POST(request: NextRequest) {
const dataClass = body.data_class ?? 'low';
const format = body.format ?? 'json';
if (pipelineStage !== 'parsed') {
return NextResponse.json(
{
error: 'DOCUMENT_NOT_PARSED',
message:
'The document has no verified text layer yet (parse_failed or not parsed). OCR is required before structuring.',
},
{ status: 422 },
);
}
if (
!isValidScopeId(projectId) ||
!isValidScopeId(documentId) ||
!isValidScopeId(versionId) ||
typeof checksum !== 'string' ||
!/^[0-9a-f]{64}$/.test(checksum) ||
pipelineStage !== 'parsed' ||
typeof instruction !== 'string' ||
!instruction.trim() ||
instruction.length > 4000 ||
+28 -9
View File
@@ -12,6 +12,7 @@ export default function CreditsPage() {
const [balance, setBalance] = useState<{ available: number; reserved: number } | null>(null);
const [balanceNote, setBalanceNote] = useState<string | null>(null);
const [packs, setPacks] = useState<Pack[]>([]);
const [purchasesEnabled, setPurchasesEnabled] = useState(true);
const [currency, setCurrency] = useState('SEK');
const [packsNote, setPacksNote] = useState<string | null>(null);
const [buying, setBuying] = useState<string | null>(null);
@@ -31,10 +32,16 @@ export default function CreditsPage() {
}
try {
const r = await fetch('/api/0pt/credits/packs', { cache: 'no-store' });
const body = (await r.json()) as { packs?: Pack[]; currency?: string; message?: string };
const body = (await r.json()) as {
packs?: Pack[];
currency?: string;
purchases_enabled?: boolean;
message?: string;
};
if (r.ok && body.packs) {
setPacks(body.packs);
setCurrency(body.currency ?? 'SEK');
setPurchasesEnabled(body.purchases_enabled !== false);
} else {
setPacksNote(
body.message ??
@@ -127,18 +134,30 @@ export default function CreditsPage() {
{formatSEK(pack.amount_minor / 100)}
</p>
</div>
<button
type="button"
onClick={() => buy(pack.id)}
disabled={buying !== null}
className="border border-foreground px-4 py-2 font-mono text-[11px] uppercase tracking-[0.18em] disabled:opacity-40"
>
{buying === pack.id ? t({ en: 'opening…', sv: 'öppnar…' }) : t({ en: 'buy', sv: 'köp' })}
</button>
{purchasesEnabled ? (
<button
type="button"
onClick={() => buy(pack.id)}
disabled={buying !== null}
className="border border-foreground px-4 py-2 font-mono text-[11px] uppercase tracking-[0.18em] disabled:opacity-40"
>
{buying === pack.id
? t({ en: 'opening…', sv: 'öppnar…' })
: t({ en: 'buy', sv: 'köp' })}
</button>
) : null}
</li>
))}
</ul>
)}
{!purchasesEnabled ? (
<p className="mt-4 text-[14px] text-muted-foreground">
{t({
en: 'Billing is not active yet (pilot). No payment can be taken.',
sv: 'Betalning är inte aktiverad än (pilot). Ingen betalning kan tas.',
})}
</p>
) : null}
{status ? <p className="mt-4 text-[14px] text-muted-foreground">{status}</p> : null}
<p className="mt-4 font-mono text-[10px] uppercase tracking-[0.16em] text-muted-foreground">
{currency}