diff --git a/schemas/listing-detail.json b/schemas/listing-detail.json index 1cc506dab..40b6c37cd 100644 --- a/schemas/listing-detail.json +++ b/schemas/listing-detail.json @@ -789,6 +789,8 @@ "token", "expiry", "created_at", + "authorization_verification", + "authorization_verified_at", "receipt_id", "tx_hash", "receipt_source", @@ -830,6 +832,14 @@ "created_at": { "type": "integer" }, + "authorization_verification": { + "type": "string", + "description": "The immutable authorization verdict recorded when this binding was filed; carried here as well as on its canonical record so omission cannot read as an unknown result." + }, + "authorization_verified_at": { + "type": "integer", + "description": "When the immutable authorization verdict was recorded, ms epoch." + }, "receipt_id": { "type": [ "integer", diff --git a/src/society.ts b/src/society.ts index 13160ccdc..0cecd99b4 100644 --- a/src/society.ts +++ b/src/society.ts @@ -4605,7 +4605,8 @@ export async function getListing(env: Env, id: number, deps: { escrowReader?: Es // (migration 0063). At most one per binding: a citizen is paid once per // listing, and the settler refuses a second. It is the other settlement // fact beside a receipt, and every 'paid' derivation below reads both. - `SELECT pb.id, pb.docket_id AS row, c.handle, pb.payout_address, pb.amount_atomic, pb.chain_id, pb.token, pb.expiry, pb.created_at, pr.id AS receipt_id, pr.tx_hash, pr.source_address AS receipt_source, + `SELECT pb.id, pb.docket_id AS row, c.handle, pb.payout_address, pb.amount_atomic, pb.chain_id, pb.token, pb.expiry, pb.created_at, + pb.authorization_verification, pb.authorization_verified_at, pr.id AS receipt_id, pr.tx_hash, pr.source_address AS receipt_source, os.id AS settled_observed_transfer_id, os.settled_award_id, os.funder_address AS observed_source, os.tx_hash AS observed_tx_hash FROM payout_bindings pb JOIN citizens c ON c.id = pb.citizen_id LEFT JOIN payout_receipts pr ON pr.binding_id = pb.id LEFT JOIN observed_transfers os ON os.binding_id = pb.id AND os.settled_award_id IS NOT NULL @@ -5082,6 +5083,10 @@ export async function getListing(env: Env, id: number, deps: { escrowReader?: Es bindings_count: results.length, bindings_total: bindingsTotal, bindings_has_more: results.length < bindingsTotal, + // Keep the binding's authorization verdict on the listing projection too. + // A null/absent summary field is not negative evidence, but it previously + // made a binding that the canonical record had verified read as unknown. + // // asset_agreement is carried on the LIST rows and not only on the single // binding record. Bindings 163 and 164 are reached far more often through // this array than by anyone fetching /api/payout-bindings/163 directly, and diff --git a/test/listing-detail-schema.test.ts b/test/listing-detail-schema.test.ts index 60380a69d..38ba5a2a6 100644 --- a/test/listing-detail-schema.test.ts +++ b/test/listing-detail-schema.test.ts @@ -189,6 +189,8 @@ function bindingRow(over: Record = {}) { token: USDC, expiry: Math.floor(now / 1000) + 3600, created_at: now - 400, + authorization_verification: "valid-at-binding-event", + authorization_verified_at: now - 400, receipt_id: null, tx_hash: null, receipt_source: null, diff --git a/test/listings.test.ts b/test/listings.test.ts index be0252bfc..134d58676 100644 --- a/test/listings.test.ts +++ b/test/listings.test.ts @@ -277,6 +277,10 @@ test("a listing is posted with its identity event, and a payee binds against it const detail = await getListing(env, 1); assert.equal(detail.bindings.length, 1); assert.equal(detail.bindings[0]!.handle, "li-nuwa"); + // The listing projection must preserve the authorization verdict from the + // canonical binding record; omitting it makes a real verdict look unknown. + assert.equal(detail.bindings[0]!.authorization_verification, "valid-at-binding-event"); + assert.equal(typeof detail.bindings[0]!.authorization_verified_at, "number"); assert.equal(detail.expired, false); const page = await listListings(env);