fix(live): refuse billing in pilot mode and explain unparsed documents
CI (SIAX Cloud) / sonar (pull_request) Skipped
masterplan-lock / check (pull_request) Successful in 3s
masterplan-lock / check (push) Successful in 14s
Security Scan (SIAX Cloud) / security (pull_request) Successful in 18s
CI (SIAX Cloud) / security (pull_request) Successful in 23s
CI (SIAX Cloud) / quality (pull_request) Successful in 54s
CI (SIAX Cloud) / integration (pull_request) Successful in 1m2s
CI (SIAX Cloud) / sonar (pull_request) Skipped
masterplan-lock / check (pull_request) Successful in 3s
masterplan-lock / check (push) Successful in 14s
Security Scan (SIAX Cloud) / security (pull_request) Successful in 18s
CI (SIAX Cloud) / security (pull_request) Successful in 23s
CI (SIAX Cloud) / quality (pull_request) Successful in 54s
CI (SIAX Cloud) / integration (pull_request) Successful in 1m2s
- Checkout returns 503 CREDITS_DISABLED while OPT_CREDITS_MODE=off, so a customer can never pay for credits that the pipeline will not consume. - Packs expose purchases_enabled; the portal hides Buy and says billing is off. - Structuring an unparsed document (e.g. a scanned PDF with no text layer) now answers 422 DOCUMENT_NOT_PARSED instead of a generic INVALID_REQUEST.
This commit is contained in:
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 ||
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user