diff --git a/crates/atproto/tests/lexicons.rs b/crates/atproto/tests/lexicons.rs index 8101ae1..b667083 100644 --- a/crates/atproto/tests/lexicons.rs +++ b/crates/atproto/tests/lexicons.rs @@ -62,6 +62,12 @@ fn assert_record_doc(doc: &Value, nsid: &str, key: &str) { fn test_storefront_lexicon() { let doc = lex("shop.lgtm.commerce.storefront"); assert_record_doc(&doc, "shop.lgtm.commerce.storefront", "literal:self"); + assert_prop_type(&doc, "storeDid", "string"); + assert_eq!( + doc["defs"]["main"]["record"]["properties"]["storeDid"]["format"], + "did", + "storeDid must be a did" + ); assert_prop_type(&doc, "displayName", "string"); assert_prop_type(&doc, "description", "string"); assert_prop_type(&doc, "currencies", "array"); @@ -71,6 +77,10 @@ fn test_storefront_lexicon() { let required = doc["defs"]["main"]["record"]["required"] .as_array() .expect("required must be an array"); + assert!( + required.iter().any(|v| v == "storeDid"), + "storeDid must be required" + ); assert!( required.iter().any(|v| v == "displayName"), "displayName must be required" @@ -303,6 +313,8 @@ fn test_purchase_claim_lexicon() { "did", "storeDid must be a did" ); + assert_prop_type(&doc, "kind", "string"); + assert_known_values(&doc, "kind", &["purchase", "membership"]); assert_prop_type(&doc, "productUri", "string"); assert_eq!( doc["defs"]["main"]["record"]["properties"]["productUri"]["format"], @@ -325,7 +337,7 @@ fn test_purchase_claim_lexicon() { let required = doc["defs"]["main"]["record"]["required"] .as_array() .expect("required must be an array"); - for want in ["storeDid", "issuedAt", "attestationRef"] { + for want in ["storeDid", "kind", "issuedAt", "attestationRef"] { assert!( required.iter().any(|v| v == want), "`{want}` must be required" @@ -423,6 +435,7 @@ fn test_order_space_lexicon() { "shop.lgtm.commerce.order", "shop.lgtm.commerce.address", "shop.lgtm.commerce.receipt", + "shop.lgtm.commerce.receiptRevocation", "shop.lgtm.commerce.fulfillmentStatus", ] { assert!( @@ -433,7 +446,7 @@ fn test_order_space_lexicon() { } #[test] -fn test_all_eleven_lexicons_present_and_valid() { +fn test_all_record_lexicons_present_and_valid() { let record_nsids = [ "shop.lgtm.commerce.storefront", "shop.lgtm.commerce.product", @@ -445,6 +458,8 @@ fn test_all_eleven_lexicons_present_and_valid() { "shop.lgtm.commerce.purchaseClaim", "shop.lgtm.commerce.purchaseAttestation", "shop.lgtm.commerce.attestationKey", + "shop.lgtm.commerce.receiptRevocation", + "shop.lgtm.commerce.relationship", ]; for nsid in record_nsids { let doc = lex(nsid); @@ -466,8 +481,8 @@ fn test_all_eleven_lexicons_present_and_valid() { space_doc["defs"]["main"]["type"], "space", "orderSpace: defs.main.type must be `space`" ); - // Total: 10 record lexicons + 1 space declaration = 11 - assert_eq!(record_nsids.len() + 1, 11, "expected exactly eleven pilot lexicons"); + // Total: 12 record lexicons + 1 space declaration = 13 + assert_eq!(record_nsids.len() + 1, 13, "expected exactly twelve record lexicons + one space"); } /// Assert a defs lexicon exposes an object def named `def_name`. @@ -637,7 +652,79 @@ fn test_create_address_method() { } #[test] -fn test_all_thirteen_methods_present_and_valid() { +fn test_receipt_revocation_lexicon() { + let doc = lex("shop.lgtm.commerce.receiptRevocation"); + assert_record_doc(&doc, "shop.lgtm.commerce.receiptRevocation", "tid"); + assert_prop_type(&doc, "receiptRef", "string"); + assert_eq!( + doc["defs"]["main"]["record"]["properties"]["receiptRef"]["format"], + "at-uri", + "receiptRef must be an at-uri" + ); + assert_prop_type(&doc, "orderRef", "string"); + assert_eq!( + doc["defs"]["main"]["record"]["properties"]["orderRef"]["format"], + "at-uri", + "orderRef must be an at-uri" + ); + assert_prop_type(&doc, "revokedAt", "string"); + assert_eq!( + doc["defs"]["main"]["record"]["properties"]["revokedAt"]["format"], + "datetime", + "revokedAt must be a datetime" + ); + assert_prop_type(&doc, "reason", "string"); + assert_known_values(&doc, "reason", &["refund", "chargeback", "dispute", "fraud", "other"]); + let required = doc["defs"]["main"]["record"]["required"] + .as_array() + .expect("required must be an array"); + for want in ["receiptRef", "revokedAt", "reason"] { + assert!( + required.iter().any(|v| v == want), + "`{want}` must be required" + ); + } +} + +#[test] +fn test_relationship_lexicon() { + let doc = lex("shop.lgtm.commerce.relationship"); + assert_record_doc(&doc, "shop.lgtm.commerce.relationship", "tid"); + assert_prop_type(&doc, "buyerDid", "string"); + assert_eq!( + doc["defs"]["main"]["record"]["properties"]["buyerDid"]["format"], + "did", + "buyerDid must be a did" + ); + assert_prop_type(&doc, "spaceRef", "string"); + assert_prop_type(&doc, "createdAt", "string"); + assert_eq!( + doc["defs"]["main"]["record"]["properties"]["createdAt"]["format"], + "datetime", + "createdAt must be a datetime" + ); + let required = doc["defs"]["main"]["record"]["required"] + .as_array() + .expect("required must be an array"); + for want in ["buyerDid", "spaceRef", "createdAt"] { + assert!( + required.iter().any(|v| v == want), + "`{want}` must be required" + ); + } +} + +#[test] +fn test_get_account_method() { + let doc = lex("shop.lgtm.commerce.getAccount"); + assert_method_doc(&doc, "shop.lgtm.commerce.getAccount", "query"); + assert_required_param(&doc, "shop"); + assert_required_param(&doc, "buyer"); + assert_json_output(&doc); +} + +#[test] +fn test_all_methods_present_and_valid() { let queries = [ "shop.lgtm.commerce.getStorefront", "shop.lgtm.commerce.listProducts", @@ -647,6 +734,7 @@ fn test_all_thirteen_methods_present_and_valid() { "shop.lgtm.commerce.getOrder", "shop.lgtm.commerce.getCart", "shop.lgtm.commerce.listAddresses", + "shop.lgtm.commerce.getAccount", ]; let procedures = [ "shop.lgtm.commerce.createCart", @@ -661,5 +749,5 @@ fn test_all_thirteen_methods_present_and_valid() { for nsid in procedures { assert_method_doc(&lex(nsid), nsid, "procedure"); } - assert_eq!(queries.len() + procedures.len(), 13, "expected exactly thirteen XRPC methods"); + assert_eq!(queries.len() + procedures.len(), 14, "expected exactly fourteen XRPC methods"); } diff --git a/lexicons/shop.lgtm.commerce.getAccount.json b/lexicons/shop.lgtm.commerce.getAccount.json new file mode 100644 index 0000000..98e95c8 --- /dev/null +++ b/lexicons/shop.lgtm.commerce.getAccount.json @@ -0,0 +1,29 @@ +{ + "lexicon": 1, + "id": "shop.lgtm.commerce.getAccount", + "defs": { + "main": { + "type": "query", + "description": "Authed companion to getView: the buyer's cart, saved addresses, and orders for this shop. Requires identity; touches permissioned/ephemeral state; NOT index-servable.", + "parameters": { + "type": "params", + "required": ["shop", "buyer"], + "properties": { + "shop": { "type": "string", "format": "did" }, + "buyer": { "type": "string", "format": "did" } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "properties": { + "cart": { "type": "ref", "ref": "shop.lgtm.commerce.defs#cartView" }, + "addresses": { "type": "array", "items": { "type": "ref", "ref": "shop.lgtm.commerce.address" } }, + "orders": { "type": "array", "items": { "type": "ref", "ref": "shop.lgtm.commerce.defs#orderView" } } + } + } + } + } + } +} diff --git a/lexicons/shop.lgtm.commerce.getView.json b/lexicons/shop.lgtm.commerce.getView.json index 4fb7929..4d0758d 100644 --- a/lexicons/shop.lgtm.commerce.getView.json +++ b/lexicons/shop.lgtm.commerce.getView.json @@ -4,7 +4,7 @@ "defs": { "main": { "type": "query", - "description": "Aggregate boot payload for a client: storefront + products, plus cart/addresses/orders when identity is present.", + "description": "Public boot payload: a shop's storefront + products. Index-servable. The authed companion is getAccount.", "parameters": { "type": "params", "required": ["shop"], @@ -17,10 +17,7 @@ "required": ["storefront", "products"], "properties": { "storefront": { "type": "ref", "ref": "shop.lgtm.commerce.defs#storefrontView" }, - "products": { "type": "array", "items": { "type": "ref", "ref": "shop.lgtm.commerce.defs#productView" } }, - "cart": { "type": "ref", "ref": "shop.lgtm.commerce.defs#cartView" }, - "addresses": { "type": "array", "items": { "type": "ref", "ref": "shop.lgtm.commerce.address" } }, - "orders": { "type": "array", "items": { "type": "ref", "ref": "shop.lgtm.commerce.defs#orderView" } } + "products": { "type": "array", "items": { "type": "ref", "ref": "shop.lgtm.commerce.defs#productView" } } } } } diff --git a/lexicons/shop.lgtm.commerce.orderSpace.json b/lexicons/shop.lgtm.commerce.orderSpace.json index e90596e..a493f79 100644 --- a/lexicons/shop.lgtm.commerce.orderSpace.json +++ b/lexicons/shop.lgtm.commerce.orderSpace.json @@ -11,6 +11,7 @@ "shop.lgtm.commerce.order", "shop.lgtm.commerce.address", "shop.lgtm.commerce.receipt", + "shop.lgtm.commerce.receiptRevocation", "shop.lgtm.commerce.fulfillmentStatus" ] } diff --git a/lexicons/shop.lgtm.commerce.purchaseClaim.json b/lexicons/shop.lgtm.commerce.purchaseClaim.json index 51b3331..18464f2 100644 --- a/lexicons/shop.lgtm.commerce.purchaseClaim.json +++ b/lexicons/shop.lgtm.commerce.purchaseClaim.json @@ -8,13 +8,18 @@ "description": "Public, buyer-owned \"I bought / I back this\" badge in the buyer's own repo (keytrace-shaped). Minimal and buyer-controlled; references the shop attestation that makes it verifiable. No buyer publish means nothing is public.", "record": { "type": "object", - "required": ["storeDid", "issuedAt", "attestationRef"], + "required": ["storeDid", "kind", "issuedAt", "attestationRef"], "properties": { "storeDid": { "type": "string", "format": "did", "description": "DID of the store this claim is about." }, + "kind": { + "type": "string", + "description": "Splits durable purchase claims from live membership status.", + "knownValues": ["purchase", "membership"] + }, "productUri": { "type": "string", "format": "at-uri", diff --git a/lexicons/shop.lgtm.commerce.receiptRevocation.json b/lexicons/shop.lgtm.commerce.receiptRevocation.json new file mode 100644 index 0000000..f6e58ca --- /dev/null +++ b/lexicons/shop.lgtm.commerce.receiptRevocation.json @@ -0,0 +1,21 @@ +{ + "lexicon": 1, + "id": "shop.lgtm.commerce.receiptRevocation", + "defs": { + "main": { + "type": "record", + "key": "tid", + "description": "Tombstone marking a receipt revoked. Store-authored, in the (store, buyer) space. Revocation writes this record; it never deletes the receipt, preserving history for buyer entitlement and merchant audit/tax.", + "record": { + "type": "object", + "required": ["receiptRef", "revokedAt", "reason"], + "properties": { + "receiptRef": { "type": "string", "format": "at-uri", "description": "Reference to the revoked receipt record." }, + "orderRef": { "type": "string", "format": "at-uri", "description": "Reference to the order the receipt settled." }, + "revokedAt": { "type": "string", "format": "datetime", "description": "When the receipt was revoked." }, + "reason": { "type": "string", "description": "Why the receipt was revoked.", "knownValues": ["refund", "chargeback", "dispute", "fraud", "other"] } + } + } + } + } +} diff --git a/lexicons/shop.lgtm.commerce.relationship.json b/lexicons/shop.lgtm.commerce.relationship.json new file mode 100644 index 0000000..92755ed --- /dev/null +++ b/lexicons/shop.lgtm.commerce.relationship.json @@ -0,0 +1,20 @@ +{ + "lexicon": 1, + "id": "shop.lgtm.commerce.relationship", + "defs": { + "main": { + "type": "record", + "key": "tid", + "description": "Store-private enumeration of a (store, buyer) relationship, in the store's own repo. Never public (it is the customer list). Makes the store's per-relationship spaces rediscoverable without the index.", + "record": { + "type": "object", + "required": ["buyerDid", "spaceRef", "createdAt"], + "properties": { + "buyerDid": { "type": "string", "format": "did", "description": "The buyer this relationship is with." }, + "spaceRef": { "type": "string", "description": "The 6-component ats:// ref of the relationship's order space." }, + "createdAt": { "type": "string", "format": "datetime", "description": "When the relationship was created." } + } + } + } + } +} diff --git a/lexicons/shop.lgtm.commerce.storefront.json b/lexicons/shop.lgtm.commerce.storefront.json index f4b99d9..04c3d47 100644 --- a/lexicons/shop.lgtm.commerce.storefront.json +++ b/lexicons/shop.lgtm.commerce.storefront.json @@ -8,8 +8,13 @@ "description": "Public, singleton shop window. Identity, branding, accepted currencies, shipping zones, and policy links for a storefront.", "record": { "type": "object", - "required": ["displayName", "currencies"], + "required": ["storeDid", "displayName", "currencies"], "properties": { + "storeDid": { + "type": "string", + "format": "did", + "description": "The store's DID. Asserting it here gives a canonical did->storefront path so a purchaseClaim's store_did resolves back to the shop." + }, "displayName": { "type": "string", "description": "Human-readable shop name shown to buyers.", diff --git a/lexicons/shop.lgtm.commerce.variant.json b/lexicons/shop.lgtm.commerce.variant.json index 7daa188..03e7ee8 100644 --- a/lexicons/shop.lgtm.commerce.variant.json +++ b/lexicons/shop.lgtm.commerce.variant.json @@ -32,22 +32,22 @@ }, "pricing": { "type": "string", - "description": "Pricing mode. `pay_what_you_want` enables a buyer-chosen amount at or above `minAmount`.", + "description": "Pricing mode. `fixed` uses `unitAmount` as the authoritative price. `pay_what_you_want` lets the buyer choose any amount at or above `minAmount`; `unitAmount` is the suggested amount. Price is always in the single native `currency`.", "knownValues": ["fixed", "pay_what_you_want"] }, "unitAmount": { "type": "integer", - "description": "Price in integer minor units of `currency` (e.g. cents for USD). For `fixed` pricing this is the price; for `pay_what_you_want` it is the suggested amount.", + "description": "The price in integer minor units of `currency` under `fixed` pricing, or the suggested amount under `pay_what_you_want`. Authoritative in the single native `currency`.", "minimum": 0 }, "minAmount": { "type": "integer", - "description": "Minimum acceptable amount in minor units for `pay_what_you_want` pricing.", + "description": "The floor amount in minor units; required under `pay_what_you_want` pricing. The buyer's chosen amount must be at or above this value.", "minimum": 0 }, "currency": { "type": "string", - "description": "ISO 4217 currency code determining the decimal places of the amounts.", + "description": "ISO 4217 currency code. All amounts (`unitAmount`, `minAmount`) are in this single native currency.", "minLength": 3, "maxLength": 3 },