Add merchant-scoped marketplace checkout plumbing
Some checks are pending
check / secretapi (push) Waiting to run
Some checks are pending
check / secretapi (push) Waiting to run
This commit is contained in:
parent
20e68c4dff
commit
b8d9147f5c
@ -397,7 +397,7 @@ func (a *app) handleMembershipQuote(w http.ResponseWriter, r *http.Request) {
|
|||||||
writeErrorCode(w, http.StatusForbidden, "sponsor_not_authorized", "sponsor entitlement is not active")
|
writeErrorCode(w, http.StatusForbidden, "sponsor_not_authorized", "sponsor entitlement is not active")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
hasWorkspaceCore, entErr := a.store.hasActiveEntitlement(r.Context(), payerAddress, offerIDWorkspaceCore, sponsorOrgRoot)
|
hasWorkspaceCore, entErr := a.store.hasActiveEntitlement(r.Context(), payerAddress, defaultMarketplaceMerchantID, offerIDWorkspaceCore, sponsorOrgRoot)
|
||||||
if entErr != nil {
|
if entErr != nil {
|
||||||
writeErrorCode(w, http.StatusInternalServerError, "store_error", "failed to validate sponsor entitlement")
|
writeErrorCode(w, http.StatusInternalServerError, "store_error", "failed to validate sponsor entitlement")
|
||||||
return
|
return
|
||||||
@ -1033,7 +1033,7 @@ func (a *app) handleGovernanceOfflineRenew(w http.ResponseWriter, r *http.Reques
|
|||||||
if isSoloRoot {
|
if isSoloRoot {
|
||||||
requiredOfferID = offerIDSoloCore
|
requiredOfferID = offerIDSoloCore
|
||||||
}
|
}
|
||||||
hasRequired, entErr := a.store.hasActiveEntitlement(r.Context(), wallet, requiredOfferID, principal.OrgRootID)
|
hasRequired, entErr := a.store.hasActiveEntitlement(r.Context(), wallet, defaultMarketplaceMerchantID, requiredOfferID, principal.OrgRootID)
|
||||||
if entErr != nil {
|
if entErr != nil {
|
||||||
writeErrorCode(w, http.StatusInternalServerError, "store_error", "failed to resolve sovereign entitlement")
|
writeErrorCode(w, http.StatusInternalServerError, "store_error", "failed to resolve sovereign entitlement")
|
||||||
return
|
return
|
||||||
|
|||||||
@ -1090,6 +1090,9 @@ func TestMarketplaceCheckoutBundlesMembershipAndMintsEntitlement(t *testing.T) {
|
|||||||
if len(quote.LineItems) < 2 {
|
if len(quote.LineItems) < 2 {
|
||||||
t.Fatalf("expected license + membership line items: %+v", quote.LineItems)
|
t.Fatalf("expected license + membership line items: %+v", quote.LineItems)
|
||||||
}
|
}
|
||||||
|
if quote.MerchantID != defaultMarketplaceMerchantID {
|
||||||
|
t.Fatalf("expected default merchant id, got %+v", quote)
|
||||||
|
}
|
||||||
|
|
||||||
confirm := postJSONExpect[marketplaceCheckoutConfirmResponse](t, a, "/marketplace/checkout/confirm", marketplaceCheckoutConfirmRequest{
|
confirm := postJSONExpect[marketplaceCheckoutConfirmResponse](t, a, "/marketplace/checkout/confirm", marketplaceCheckoutConfirmRequest{
|
||||||
QuoteID: quote.QuoteID,
|
QuoteID: quote.QuoteID,
|
||||||
@ -1117,6 +1120,90 @@ func TestMarketplaceCheckoutBundlesMembershipAndMintsEntitlement(t *testing.T) {
|
|||||||
if entitlements.Entitlements[0].OfferID != offerIDWorkspaceCore {
|
if entitlements.Entitlements[0].OfferID != offerIDWorkspaceCore {
|
||||||
t.Fatalf("unexpected entitlement offer: %+v", entitlements.Entitlements[0])
|
t.Fatalf("unexpected entitlement offer: %+v", entitlements.Entitlements[0])
|
||||||
}
|
}
|
||||||
|
if entitlements.Entitlements[0].MerchantID != defaultMarketplaceMerchantID {
|
||||||
|
t.Fatalf("expected default merchant id on entitlement, got %+v", entitlements.Entitlements[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMarketplaceQuoteRejectsUnknownMerchant(t *testing.T) {
|
||||||
|
a, _, cleanup := newTestApp(t)
|
||||||
|
defer cleanup()
|
||||||
|
|
||||||
|
ownerKey := mustKey(t)
|
||||||
|
ownerAddr := strings.ToLower(crypto.PubkeyToAddress(ownerKey.PublicKey).Hex())
|
||||||
|
if err := seedActiveMembership(context.Background(), a.store, ownerAddr); err != nil {
|
||||||
|
t.Fatalf("seed active membership: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
errResp := postJSONExpect[map[string]string](t, a, "/marketplace/checkout/quote", marketplaceCheckoutQuoteRequest{
|
||||||
|
MerchantID: "acme.workspace",
|
||||||
|
Wallet: ownerAddr,
|
||||||
|
OfferID: offerIDWorkspaceCore,
|
||||||
|
OrgRootID: "org.marketplace.acme",
|
||||||
|
PrincipalID: "human.owner",
|
||||||
|
PrincipalRole: "org_root_owner",
|
||||||
|
}, http.StatusNotFound)
|
||||||
|
if code := errResp["code"]; code != "offer_not_found" {
|
||||||
|
t.Fatalf("expected offer_not_found for unknown merchant, got %+v", errResp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMarketplaceEntitlementsFilterByMerchant(t *testing.T) {
|
||||||
|
a, _, cleanup := newTestApp(t)
|
||||||
|
defer cleanup()
|
||||||
|
|
||||||
|
ownerKey := mustKey(t)
|
||||||
|
ownerAddr := strings.ToLower(crypto.PubkeyToAddress(ownerKey.PublicKey).Hex())
|
||||||
|
now := time.Now().UTC()
|
||||||
|
if err := a.store.putMarketplaceEntitlement(context.Background(), marketplaceEntitlementRecord{
|
||||||
|
EntitlementID: "ent:seed:firstparty",
|
||||||
|
QuoteID: "seed-q-firstparty",
|
||||||
|
MerchantID: defaultMarketplaceMerchantID,
|
||||||
|
OfferID: offerIDWorkspaceCore,
|
||||||
|
Wallet: ownerAddr,
|
||||||
|
OrgRootID: "org.marketplace.filter",
|
||||||
|
PrincipalID: "human.owner",
|
||||||
|
PrincipalRole: "org_root_owner",
|
||||||
|
State: "active",
|
||||||
|
AccessClass: "connected",
|
||||||
|
AvailabilityState: "active",
|
||||||
|
PolicyHash: "sha256:testpolicy",
|
||||||
|
IssuedAt: now,
|
||||||
|
TxHash: "0xcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc",
|
||||||
|
}); err != nil {
|
||||||
|
t.Fatalf("seed first-party entitlement: %v", err)
|
||||||
|
}
|
||||||
|
if err := a.store.putMarketplaceEntitlement(context.Background(), marketplaceEntitlementRecord{
|
||||||
|
EntitlementID: "ent:seed:other",
|
||||||
|
QuoteID: "seed-q-other",
|
||||||
|
MerchantID: "acme.workspace",
|
||||||
|
OfferID: "acme.offer.alpha",
|
||||||
|
Wallet: ownerAddr,
|
||||||
|
OrgRootID: "org.marketplace.filter",
|
||||||
|
PrincipalID: "human.owner",
|
||||||
|
PrincipalRole: "org_root_owner",
|
||||||
|
State: "active",
|
||||||
|
AccessClass: "connected",
|
||||||
|
AvailabilityState: "active",
|
||||||
|
PolicyHash: "sha256:testpolicy",
|
||||||
|
IssuedAt: now,
|
||||||
|
TxHash: "0xdddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd",
|
||||||
|
}); err != nil {
|
||||||
|
t.Fatalf("seed third-party entitlement: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
all := getJSONExpect[marketplaceEntitlementsResponse](t, a, "/marketplace/entitlements?wallet="+ownerAddr, http.StatusOK)
|
||||||
|
if len(all.Entitlements) != 2 {
|
||||||
|
t.Fatalf("expected both merchant entitlements, got %+v", all.Entitlements)
|
||||||
|
}
|
||||||
|
|
||||||
|
firstParty := getJSONExpect[marketplaceEntitlementsResponse](t, a, "/marketplace/entitlements?wallet="+ownerAddr+"&merchant_id="+defaultMarketplaceMerchantID, http.StatusOK)
|
||||||
|
if len(firstParty.Entitlements) != 1 {
|
||||||
|
t.Fatalf("expected one first-party entitlement, got %+v", firstParty.Entitlements)
|
||||||
|
}
|
||||||
|
if firstParty.Entitlements[0].MerchantID != defaultMarketplaceMerchantID {
|
||||||
|
t.Fatalf("expected first-party merchant id, got %+v", firstParty.Entitlements[0])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMarketplaceDistinctPayerRequiresOwnershipProof(t *testing.T) {
|
func TestMarketplaceDistinctPayerRequiresOwnershipProof(t *testing.T) {
|
||||||
|
|||||||
@ -16,6 +16,7 @@ import (
|
|||||||
const (
|
const (
|
||||||
marketplaceMembershipActivationAtomic = "100000000" // 100.00 USDC (6 decimals)
|
marketplaceMembershipActivationAtomic = "100000000" // 100.00 USDC (6 decimals)
|
||||||
marketplaceStandardOfferAtomic = "1000000000" // 1000.00 USDC (6 decimals)
|
marketplaceStandardOfferAtomic = "1000000000" // 1000.00 USDC (6 decimals)
|
||||||
|
defaultMarketplaceMerchantID = "edut.firstparty"
|
||||||
|
|
||||||
offerIDSoloCore = "edut.solo.core"
|
offerIDSoloCore = "edut.solo.core"
|
||||||
offerIDWorkspaceCore = "edut.workspace.core"
|
offerIDWorkspaceCore = "edut.workspace.core"
|
||||||
@ -24,11 +25,33 @@ const (
|
|||||||
offerIDWorkspaceSovereign = "edut.workspace.sovereign"
|
offerIDWorkspaceSovereign = "edut.workspace.sovereign"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (a *app) marketplaceOffers() []marketplaceOffer {
|
func normalizeMerchantID(raw string) string {
|
||||||
|
merchantID := strings.ToLower(strings.TrimSpace(raw))
|
||||||
|
if merchantID == "" {
|
||||||
|
return defaultMarketplaceMerchantID
|
||||||
|
}
|
||||||
|
return merchantID
|
||||||
|
}
|
||||||
|
|
||||||
|
func marketplaceContractOfferID(merchantID, offerID string) string {
|
||||||
|
merchantID = normalizeMerchantID(merchantID)
|
||||||
|
offerID = strings.TrimSpace(strings.ToLower(offerID))
|
||||||
|
if merchantID == defaultMarketplaceMerchantID {
|
||||||
|
return offerID
|
||||||
|
}
|
||||||
|
return merchantID + ":" + offerID
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *app) marketplaceOffersForMerchant(merchantID string) []marketplaceOffer {
|
||||||
|
merchantID = normalizeMerchantID(merchantID)
|
||||||
|
if merchantID != defaultMarketplaceMerchantID {
|
||||||
|
return []marketplaceOffer{}
|
||||||
|
}
|
||||||
offers := []marketplaceOffer{
|
offers := []marketplaceOffer{
|
||||||
{
|
{
|
||||||
|
MerchantID: defaultMarketplaceMerchantID,
|
||||||
OfferID: offerIDSoloCore,
|
OfferID: offerIDSoloCore,
|
||||||
IssuerID: "edut.firstparty",
|
IssuerID: defaultMarketplaceMerchantID,
|
||||||
Title: "EDUT Solo Core",
|
Title: "EDUT Solo Core",
|
||||||
Summary: "Single-principal governance runtime for personal operations.",
|
Summary: "Single-principal governance runtime for personal operations.",
|
||||||
Status: "active",
|
Status: "active",
|
||||||
@ -54,8 +77,9 @@ func (a *app) marketplaceOffers() []marketplaceOffer {
|
|||||||
SortOrder: 10,
|
SortOrder: 10,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
MerchantID: defaultMarketplaceMerchantID,
|
||||||
OfferID: offerIDWorkspaceCore,
|
OfferID: offerIDWorkspaceCore,
|
||||||
IssuerID: "edut.firstparty",
|
IssuerID: defaultMarketplaceMerchantID,
|
||||||
Title: "EDUT Workspace Core",
|
Title: "EDUT Workspace Core",
|
||||||
Summary: "Org-bound deterministic governance runtime for team operations.",
|
Summary: "Org-bound deterministic governance runtime for team operations.",
|
||||||
Status: "active",
|
Status: "active",
|
||||||
@ -81,8 +105,9 @@ func (a *app) marketplaceOffers() []marketplaceOffer {
|
|||||||
SortOrder: 20,
|
SortOrder: 20,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
MerchantID: defaultMarketplaceMerchantID,
|
||||||
OfferID: offerIDWorkspaceAI,
|
OfferID: offerIDWorkspaceAI,
|
||||||
IssuerID: "edut.firstparty",
|
IssuerID: defaultMarketplaceMerchantID,
|
||||||
Title: "EDUT Workspace AI Layer",
|
Title: "EDUT Workspace AI Layer",
|
||||||
Summary: "AI reasoning layer for governed workspace operations.",
|
Summary: "AI reasoning layer for governed workspace operations.",
|
||||||
Status: "active",
|
Status: "active",
|
||||||
@ -109,8 +134,9 @@ func (a *app) marketplaceOffers() []marketplaceOffer {
|
|||||||
SortOrder: 30,
|
SortOrder: 30,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
MerchantID: defaultMarketplaceMerchantID,
|
||||||
OfferID: offerIDWorkspaceLane24,
|
OfferID: offerIDWorkspaceLane24,
|
||||||
IssuerID: "edut.firstparty",
|
IssuerID: defaultMarketplaceMerchantID,
|
||||||
Title: "EDUT Workspace 24h Lane",
|
Title: "EDUT Workspace 24h Lane",
|
||||||
Summary: "Autonomous execution lane capacity for workspace queue throughput.",
|
Summary: "Autonomous execution lane capacity for workspace queue throughput.",
|
||||||
Status: "active",
|
Status: "active",
|
||||||
@ -136,8 +162,9 @@ func (a *app) marketplaceOffers() []marketplaceOffer {
|
|||||||
SortOrder: 40,
|
SortOrder: 40,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
MerchantID: defaultMarketplaceMerchantID,
|
||||||
OfferID: offerIDWorkspaceSovereign,
|
OfferID: offerIDWorkspaceSovereign,
|
||||||
IssuerID: "edut.firstparty",
|
IssuerID: defaultMarketplaceMerchantID,
|
||||||
Title: "EDUT Workspace Sovereign Continuity",
|
Title: "EDUT Workspace Sovereign Continuity",
|
||||||
Summary: "Workspace continuity profile for stronger local/offline operation.",
|
Summary: "Workspace continuity profile for stronger local/offline operation.",
|
||||||
Status: "active",
|
Status: "active",
|
||||||
@ -169,9 +196,13 @@ func (a *app) marketplaceOffers() []marketplaceOffer {
|
|||||||
return offers
|
return offers
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *app) marketplaceOfferByID(offerID string) (marketplaceOffer, error) {
|
func (a *app) marketplaceOffers() []marketplaceOffer {
|
||||||
|
return a.marketplaceOffersForMerchant(defaultMarketplaceMerchantID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *app) marketplaceOfferByID(merchantID, offerID string) (marketplaceOffer, error) {
|
||||||
target := strings.TrimSpace(strings.ToLower(offerID))
|
target := strings.TrimSpace(strings.ToLower(offerID))
|
||||||
for _, offer := range a.marketplaceOffers() {
|
for _, offer := range a.marketplaceOffersForMerchant(merchantID) {
|
||||||
if strings.EqualFold(strings.TrimSpace(offer.OfferID), target) {
|
if strings.EqualFold(strings.TrimSpace(offer.OfferID), target) {
|
||||||
return offer, nil
|
return offer, nil
|
||||||
}
|
}
|
||||||
@ -210,7 +241,8 @@ func (a *app) handleMarketplaceOffers(w http.ResponseWriter, r *http.Request) {
|
|||||||
writeError(w, http.StatusMethodNotAllowed, "method not allowed")
|
writeError(w, http.StatusMethodNotAllowed, "method not allowed")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
writeJSON(w, http.StatusOK, marketplaceOffersResponse{Offers: a.marketplaceOffers()})
|
merchantID := normalizeMerchantID(r.URL.Query().Get("merchant_id"))
|
||||||
|
writeJSON(w, http.StatusOK, marketplaceOffersResponse{Offers: a.marketplaceOffersForMerchant(merchantID)})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *app) handleMarketplaceOfferByID(w http.ResponseWriter, r *http.Request) {
|
func (a *app) handleMarketplaceOfferByID(w http.ResponseWriter, r *http.Request) {
|
||||||
@ -224,7 +256,8 @@ func (a *app) handleMarketplaceOfferByID(w http.ResponseWriter, r *http.Request)
|
|||||||
writeErrorCode(w, http.StatusNotFound, "offer_not_found", "offer not found")
|
writeErrorCode(w, http.StatusNotFound, "offer_not_found", "offer not found")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
offer, err := a.marketplaceOfferByID(offerID)
|
merchantID := normalizeMerchantID(r.URL.Query().Get("merchant_id"))
|
||||||
|
offer, err := a.marketplaceOfferByID(merchantID, offerID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeErrorCode(w, http.StatusNotFound, "offer_not_found", "offer not found")
|
writeErrorCode(w, http.StatusNotFound, "offer_not_found", "offer not found")
|
||||||
return
|
return
|
||||||
@ -250,7 +283,8 @@ func (a *app) handleMarketplaceCheckoutQuote(w http.ResponseWriter, r *http.Requ
|
|||||||
if !a.enforceWalletSession(w, r, wallet) {
|
if !a.enforceWalletSession(w, r, wallet) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
offer, err := a.marketplaceOfferByID(req.OfferID)
|
merchantID := normalizeMerchantID(req.MerchantID)
|
||||||
|
offer, err := a.marketplaceOfferByID(merchantID, req.OfferID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeErrorCode(w, http.StatusNotFound, "offer_not_found", "offer not found")
|
writeErrorCode(w, http.StatusNotFound, "offer_not_found", "offer not found")
|
||||||
return
|
return
|
||||||
@ -313,7 +347,7 @@ func (a *app) handleMarketplaceCheckoutQuote(w http.ResponseWriter, r *http.Requ
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, requiredOfferID := range offer.Policies.RequiresOffers {
|
for _, requiredOfferID := range offer.Policies.RequiresOffers {
|
||||||
hasRequired, err := a.store.hasActiveEntitlement(r.Context(), wallet, requiredOfferID, orgRootID)
|
hasRequired, err := a.store.hasActiveEntitlement(r.Context(), wallet, merchantID, requiredOfferID, orgRootID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeErrorCode(w, http.StatusInternalServerError, "store_error", "failed to resolve prerequisite entitlements")
|
writeErrorCode(w, http.StatusInternalServerError, "store_error", "failed to resolve prerequisite entitlements")
|
||||||
return
|
return
|
||||||
@ -410,7 +444,7 @@ func (a *app) handleMarketplaceCheckoutQuote(w http.ResponseWriter, r *http.Requ
|
|||||||
|
|
||||||
txTo := entitlementContract
|
txTo := entitlementContract
|
||||||
txValueHex := "0x0"
|
txValueHex := "0x0"
|
||||||
txData, calldataErr := encodePurchaseEntitlementCalldata(offer.OfferID, wallet, orgRootID, workspaceID)
|
txData, calldataErr := encodePurchaseEntitlementCalldata(marketplaceContractOfferID(merchantID, offer.OfferID), wallet, orgRootID, workspaceID)
|
||||||
if calldataErr != nil {
|
if calldataErr != nil {
|
||||||
writeErrorCode(w, http.StatusInternalServerError, "quote_generation_failed", "failed to encode checkout transaction")
|
writeErrorCode(w, http.StatusInternalServerError, "quote_generation_failed", "failed to encode checkout transaction")
|
||||||
return
|
return
|
||||||
@ -424,6 +458,7 @@ func (a *app) handleMarketplaceCheckoutQuote(w http.ResponseWriter, r *http.Requ
|
|||||||
}
|
}
|
||||||
quote := marketplaceQuoteRecord{
|
quote := marketplaceQuoteRecord{
|
||||||
QuoteID: "cq_" + quoteIDRaw,
|
QuoteID: "cq_" + quoteIDRaw,
|
||||||
|
MerchantID: merchantID,
|
||||||
Wallet: wallet,
|
Wallet: wallet,
|
||||||
PayerWallet: payerWallet,
|
PayerWallet: payerWallet,
|
||||||
OfferID: offer.OfferID,
|
OfferID: offer.OfferID,
|
||||||
@ -453,6 +488,7 @@ func (a *app) handleMarketplaceCheckoutQuote(w http.ResponseWriter, r *http.Requ
|
|||||||
|
|
||||||
writeJSON(w, http.StatusOK, marketplaceCheckoutQuoteResponse{
|
writeJSON(w, http.StatusOK, marketplaceCheckoutQuoteResponse{
|
||||||
QuoteID: quote.QuoteID,
|
QuoteID: quote.QuoteID,
|
||||||
|
MerchantID: quote.MerchantID,
|
||||||
Wallet: quote.Wallet,
|
Wallet: quote.Wallet,
|
||||||
PayerWallet: quote.PayerWallet,
|
PayerWallet: quote.PayerWallet,
|
||||||
OfferID: quote.OfferID,
|
OfferID: quote.OfferID,
|
||||||
@ -527,6 +563,11 @@ func (a *app) handleMarketplaceCheckoutConfirm(w http.ResponseWriter, r *http.Re
|
|||||||
writeErrorCode(w, http.StatusConflict, "quote_expired", "quote expired")
|
writeErrorCode(w, http.StatusConflict, "quote_expired", "quote expired")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
merchantID := normalizeMerchantID(req.MerchantID)
|
||||||
|
if !strings.EqualFold(quote.MerchantID, merchantID) {
|
||||||
|
writeErrorCode(w, http.StatusBadRequest, "context_mismatch", "merchant_id mismatch")
|
||||||
|
return
|
||||||
|
}
|
||||||
if !strings.EqualFold(quote.Wallet, wallet) ||
|
if !strings.EqualFold(quote.Wallet, wallet) ||
|
||||||
!strings.EqualFold(strings.TrimSpace(quote.OfferID), strings.TrimSpace(req.OfferID)) {
|
!strings.EqualFold(strings.TrimSpace(quote.OfferID), strings.TrimSpace(req.OfferID)) {
|
||||||
writeErrorCode(w, http.StatusBadRequest, "context_mismatch", "quote context mismatch")
|
writeErrorCode(w, http.StatusBadRequest, "context_mismatch", "quote context mismatch")
|
||||||
@ -566,6 +607,7 @@ func (a *app) handleMarketplaceCheckoutConfirm(w http.ResponseWriter, r *http.Re
|
|||||||
writeJSON(w, http.StatusOK, marketplaceCheckoutConfirmResponse{
|
writeJSON(w, http.StatusOK, marketplaceCheckoutConfirmResponse{
|
||||||
Status: "entitlement_active",
|
Status: "entitlement_active",
|
||||||
EntitlementID: existing.EntitlementID,
|
EntitlementID: existing.EntitlementID,
|
||||||
|
MerchantID: existing.MerchantID,
|
||||||
OfferID: existing.OfferID,
|
OfferID: existing.OfferID,
|
||||||
OrgRootID: existing.OrgRootID,
|
OrgRootID: existing.OrgRootID,
|
||||||
PrincipalID: existing.PrincipalID,
|
PrincipalID: existing.PrincipalID,
|
||||||
@ -633,6 +675,7 @@ func (a *app) handleMarketplaceCheckoutConfirm(w http.ResponseWriter, r *http.Re
|
|||||||
ent := marketplaceEntitlementRecord{
|
ent := marketplaceEntitlementRecord{
|
||||||
EntitlementID: entitlementID,
|
EntitlementID: entitlementID,
|
||||||
QuoteID: quote.QuoteID,
|
QuoteID: quote.QuoteID,
|
||||||
|
MerchantID: quote.MerchantID,
|
||||||
OfferID: quote.OfferID,
|
OfferID: quote.OfferID,
|
||||||
Wallet: wallet,
|
Wallet: wallet,
|
||||||
PayerWallet: quote.PayerWallet,
|
PayerWallet: quote.PayerWallet,
|
||||||
@ -667,6 +710,7 @@ func (a *app) handleMarketplaceCheckoutConfirm(w http.ResponseWriter, r *http.Re
|
|||||||
writeJSON(w, http.StatusOK, marketplaceCheckoutConfirmResponse{
|
writeJSON(w, http.StatusOK, marketplaceCheckoutConfirmResponse{
|
||||||
Status: "entitlement_active",
|
Status: "entitlement_active",
|
||||||
EntitlementID: ent.EntitlementID,
|
EntitlementID: ent.EntitlementID,
|
||||||
|
MerchantID: ent.MerchantID,
|
||||||
OfferID: ent.OfferID,
|
OfferID: ent.OfferID,
|
||||||
OrgRootID: ent.OrgRootID,
|
OrgRootID: ent.OrgRootID,
|
||||||
PrincipalID: ent.PrincipalID,
|
PrincipalID: ent.PrincipalID,
|
||||||
@ -693,7 +737,8 @@ func (a *app) handleMarketplaceEntitlements(w http.ResponseWriter, r *http.Reque
|
|||||||
if !a.enforceWalletSession(w, r, wallet) {
|
if !a.enforceWalletSession(w, r, wallet) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
records, err := a.store.listMarketplaceEntitlementsByWallet(r.Context(), wallet)
|
merchantID := strings.TrimSpace(r.URL.Query().Get("merchant_id"))
|
||||||
|
records, err := a.store.listMarketplaceEntitlementsByWallet(r.Context(), wallet, merchantID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeErrorCode(w, http.StatusInternalServerError, "store_error", "failed to list entitlements")
|
writeErrorCode(w, http.StatusInternalServerError, "store_error", "failed to list entitlements")
|
||||||
return
|
return
|
||||||
@ -702,6 +747,7 @@ func (a *app) handleMarketplaceEntitlements(w http.ResponseWriter, r *http.Reque
|
|||||||
for _, rec := range records {
|
for _, rec := range records {
|
||||||
out = append(out, marketplaceEntitlement{
|
out = append(out, marketplaceEntitlement{
|
||||||
EntitlementID: rec.EntitlementID,
|
EntitlementID: rec.EntitlementID,
|
||||||
|
MerchantID: rec.MerchantID,
|
||||||
OfferID: rec.OfferID,
|
OfferID: rec.OfferID,
|
||||||
WalletAddress: rec.Wallet,
|
WalletAddress: rec.Wallet,
|
||||||
WorkspaceID: rec.WorkspaceID,
|
WorkspaceID: rec.WorkspaceID,
|
||||||
|
|||||||
@ -3,6 +3,7 @@ package main
|
|||||||
import "time"
|
import "time"
|
||||||
|
|
||||||
type marketplaceOffer struct {
|
type marketplaceOffer struct {
|
||||||
|
MerchantID string `json:"merchant_id,omitempty"`
|
||||||
OfferID string `json:"offer_id"`
|
OfferID string `json:"offer_id"`
|
||||||
IssuerID string `json:"issuer_id"`
|
IssuerID string `json:"issuer_id"`
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
@ -42,6 +43,7 @@ type marketplaceOffersResponse struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type marketplaceCheckoutQuoteRequest struct {
|
type marketplaceCheckoutQuoteRequest struct {
|
||||||
|
MerchantID string `json:"merchant_id,omitempty"`
|
||||||
Wallet string `json:"wallet"`
|
Wallet string `json:"wallet"`
|
||||||
PayerWallet string `json:"payer_wallet,omitempty"`
|
PayerWallet string `json:"payer_wallet,omitempty"`
|
||||||
OfferID string `json:"offer_id"`
|
OfferID string `json:"offer_id"`
|
||||||
@ -64,6 +66,7 @@ type marketplaceQuoteLineItem struct {
|
|||||||
|
|
||||||
type marketplaceCheckoutQuoteResponse struct {
|
type marketplaceCheckoutQuoteResponse struct {
|
||||||
QuoteID string `json:"quote_id"`
|
QuoteID string `json:"quote_id"`
|
||||||
|
MerchantID string `json:"merchant_id,omitempty"`
|
||||||
Wallet string `json:"wallet"`
|
Wallet string `json:"wallet"`
|
||||||
PayerWallet string `json:"payer_wallet,omitempty"`
|
PayerWallet string `json:"payer_wallet,omitempty"`
|
||||||
OfferID string `json:"offer_id"`
|
OfferID string `json:"offer_id"`
|
||||||
@ -87,6 +90,7 @@ type marketplaceCheckoutQuoteResponse struct {
|
|||||||
|
|
||||||
type marketplaceCheckoutConfirmRequest struct {
|
type marketplaceCheckoutConfirmRequest struct {
|
||||||
QuoteID string `json:"quote_id"`
|
QuoteID string `json:"quote_id"`
|
||||||
|
MerchantID string `json:"merchant_id,omitempty"`
|
||||||
Wallet string `json:"wallet"`
|
Wallet string `json:"wallet"`
|
||||||
PayerWallet string `json:"payer_wallet,omitempty"`
|
PayerWallet string `json:"payer_wallet,omitempty"`
|
||||||
OfferID string `json:"offer_id"`
|
OfferID string `json:"offer_id"`
|
||||||
@ -101,6 +105,7 @@ type marketplaceCheckoutConfirmRequest struct {
|
|||||||
type marketplaceCheckoutConfirmResponse struct {
|
type marketplaceCheckoutConfirmResponse struct {
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
EntitlementID string `json:"entitlement_id"`
|
EntitlementID string `json:"entitlement_id"`
|
||||||
|
MerchantID string `json:"merchant_id,omitempty"`
|
||||||
OfferID string `json:"offer_id"`
|
OfferID string `json:"offer_id"`
|
||||||
OrgRootID string `json:"org_root_id,omitempty"`
|
OrgRootID string `json:"org_root_id,omitempty"`
|
||||||
PrincipalID string `json:"principal_id,omitempty"`
|
PrincipalID string `json:"principal_id,omitempty"`
|
||||||
@ -115,6 +120,7 @@ type marketplaceCheckoutConfirmResponse struct {
|
|||||||
|
|
||||||
type marketplaceEntitlement struct {
|
type marketplaceEntitlement struct {
|
||||||
EntitlementID string `json:"entitlement_id"`
|
EntitlementID string `json:"entitlement_id"`
|
||||||
|
MerchantID string `json:"merchant_id,omitempty"`
|
||||||
OfferID string `json:"offer_id"`
|
OfferID string `json:"offer_id"`
|
||||||
WalletAddress string `json:"wallet_address"`
|
WalletAddress string `json:"wallet_address"`
|
||||||
WorkspaceID string `json:"workspace_id,omitempty"`
|
WorkspaceID string `json:"workspace_id,omitempty"`
|
||||||
@ -132,6 +138,7 @@ type marketplaceEntitlementsResponse struct {
|
|||||||
|
|
||||||
type marketplaceQuoteRecord struct {
|
type marketplaceQuoteRecord struct {
|
||||||
QuoteID string
|
QuoteID string
|
||||||
|
MerchantID string
|
||||||
Wallet string
|
Wallet string
|
||||||
PayerWallet string
|
PayerWallet string
|
||||||
OfferID string
|
OfferID string
|
||||||
@ -160,6 +167,7 @@ type marketplaceQuoteRecord struct {
|
|||||||
type marketplaceEntitlementRecord struct {
|
type marketplaceEntitlementRecord struct {
|
||||||
EntitlementID string
|
EntitlementID string
|
||||||
QuoteID string
|
QuoteID string
|
||||||
|
MerchantID string
|
||||||
OfferID string
|
OfferID string
|
||||||
Wallet string
|
Wallet string
|
||||||
PayerWallet string
|
PayerWallet string
|
||||||
|
|||||||
@ -99,6 +99,7 @@ func (s *store) migrate(ctx context.Context) error {
|
|||||||
`CREATE INDEX IF NOT EXISTS idx_quotes_confirmed_tx_hash ON quotes(confirmed_tx_hash);`,
|
`CREATE INDEX IF NOT EXISTS idx_quotes_confirmed_tx_hash ON quotes(confirmed_tx_hash);`,
|
||||||
`CREATE TABLE IF NOT EXISTS marketplace_quotes (
|
`CREATE TABLE IF NOT EXISTS marketplace_quotes (
|
||||||
quote_id TEXT PRIMARY KEY,
|
quote_id TEXT PRIMARY KEY,
|
||||||
|
merchant_id TEXT NOT NULL DEFAULT 'edut.firstparty',
|
||||||
wallet TEXT NOT NULL,
|
wallet TEXT NOT NULL,
|
||||||
payer_wallet TEXT,
|
payer_wallet TEXT,
|
||||||
offer_id TEXT NOT NULL,
|
offer_id TEXT NOT NULL,
|
||||||
@ -128,6 +129,7 @@ func (s *store) migrate(ctx context.Context) error {
|
|||||||
`CREATE TABLE IF NOT EXISTS marketplace_entitlements (
|
`CREATE TABLE IF NOT EXISTS marketplace_entitlements (
|
||||||
entitlement_id TEXT PRIMARY KEY,
|
entitlement_id TEXT PRIMARY KEY,
|
||||||
quote_id TEXT NOT NULL UNIQUE,
|
quote_id TEXT NOT NULL UNIQUE,
|
||||||
|
merchant_id TEXT NOT NULL DEFAULT 'edut.firstparty',
|
||||||
offer_id TEXT NOT NULL,
|
offer_id TEXT NOT NULL,
|
||||||
wallet TEXT NOT NULL,
|
wallet TEXT NOT NULL,
|
||||||
payer_wallet TEXT,
|
payer_wallet TEXT,
|
||||||
@ -143,6 +145,7 @@ func (s *store) migrate(ctx context.Context) error {
|
|||||||
tx_hash TEXT NOT NULL
|
tx_hash TEXT NOT NULL
|
||||||
);`,
|
);`,
|
||||||
`CREATE INDEX IF NOT EXISTS idx_marketplace_entitlements_wallet ON marketplace_entitlements(wallet);`,
|
`CREATE INDEX IF NOT EXISTS idx_marketplace_entitlements_wallet ON marketplace_entitlements(wallet);`,
|
||||||
|
`CREATE INDEX IF NOT EXISTS idx_marketplace_entitlements_wallet_merchant_offer_state ON marketplace_entitlements(wallet, merchant_id, offer_id, state);`,
|
||||||
`CREATE TABLE IF NOT EXISTS governance_principals (
|
`CREATE TABLE IF NOT EXISTS governance_principals (
|
||||||
wallet TEXT PRIMARY KEY,
|
wallet TEXT PRIMARY KEY,
|
||||||
org_root_id TEXT NOT NULL,
|
org_root_id TEXT NOT NULL,
|
||||||
@ -282,6 +285,12 @@ func (s *store) migrate(ctx context.Context) error {
|
|||||||
if err := s.ensureColumn(ctx, "marketplace_quotes", "expected_tx_value_hex", "TEXT"); err != nil {
|
if err := s.ensureColumn(ctx, "marketplace_quotes", "expected_tx_value_hex", "TEXT"); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if err := s.ensureColumn(ctx, "marketplace_quotes", "merchant_id", "TEXT NOT NULL DEFAULT 'edut.firstparty'"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := s.ensureColumn(ctx, "marketplace_entitlements", "merchant_id", "TEXT NOT NULL DEFAULT 'edut.firstparty'"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -588,11 +597,12 @@ func (s *store) getDesignationCodeByMembershipTxHash(ctx context.Context, txHash
|
|||||||
func (s *store) putMarketplaceQuote(ctx context.Context, quote marketplaceQuoteRecord) error {
|
func (s *store) putMarketplaceQuote(ctx context.Context, quote marketplaceQuoteRecord) error {
|
||||||
_, err := s.db.ExecContext(ctx, `
|
_, err := s.db.ExecContext(ctx, `
|
||||||
INSERT INTO marketplace_quotes (
|
INSERT INTO marketplace_quotes (
|
||||||
quote_id, wallet, payer_wallet, offer_id, org_root_id, principal_id, principal_role, workspace_id,
|
quote_id, merchant_id, wallet, payer_wallet, offer_id, org_root_id, principal_id, principal_role, workspace_id,
|
||||||
currency, amount_atomic, total_amount_atomic, decimals, membership_included, line_items_json,
|
currency, amount_atomic, total_amount_atomic, decimals, membership_included, line_items_json,
|
||||||
policy_hash, access_class, availability_state, expected_tx_to, expected_tx_data, expected_tx_value_hex, created_at, expires_at, confirmed_at, confirmed_tx_hash
|
policy_hash, access_class, availability_state, expected_tx_to, expected_tx_data, expected_tx_value_hex, created_at, expires_at, confirmed_at, confirmed_tx_hash
|
||||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
ON CONFLICT(quote_id) DO UPDATE SET
|
ON CONFLICT(quote_id) DO UPDATE SET
|
||||||
|
merchant_id=excluded.merchant_id,
|
||||||
wallet=excluded.wallet,
|
wallet=excluded.wallet,
|
||||||
payer_wallet=excluded.payer_wallet,
|
payer_wallet=excluded.payer_wallet,
|
||||||
offer_id=excluded.offer_id,
|
offer_id=excluded.offer_id,
|
||||||
@ -617,6 +627,7 @@ func (s *store) putMarketplaceQuote(ctx context.Context, quote marketplaceQuoteR
|
|||||||
confirmed_at=excluded.confirmed_at,
|
confirmed_at=excluded.confirmed_at,
|
||||||
confirmed_tx_hash=excluded.confirmed_tx_hash
|
confirmed_tx_hash=excluded.confirmed_tx_hash
|
||||||
`, quote.QuoteID,
|
`, quote.QuoteID,
|
||||||
|
normalizeMerchantID(quote.MerchantID),
|
||||||
strings.ToLower(strings.TrimSpace(quote.Wallet)),
|
strings.ToLower(strings.TrimSpace(quote.Wallet)),
|
||||||
nullableString(strings.ToLower(strings.TrimSpace(quote.PayerWallet))),
|
nullableString(strings.ToLower(strings.TrimSpace(quote.PayerWallet))),
|
||||||
strings.TrimSpace(quote.OfferID),
|
strings.TrimSpace(quote.OfferID),
|
||||||
@ -646,19 +657,20 @@ func (s *store) putMarketplaceQuote(ctx context.Context, quote marketplaceQuoteR
|
|||||||
|
|
||||||
func (s *store) getMarketplaceQuote(ctx context.Context, quoteID string) (marketplaceQuoteRecord, error) {
|
func (s *store) getMarketplaceQuote(ctx context.Context, quoteID string) (marketplaceQuoteRecord, error) {
|
||||||
row := s.db.QueryRowContext(ctx, `
|
row := s.db.QueryRowContext(ctx, `
|
||||||
SELECT quote_id, wallet, payer_wallet, offer_id, org_root_id, principal_id, principal_role, workspace_id,
|
SELECT quote_id, merchant_id, wallet, payer_wallet, offer_id, org_root_id, principal_id, principal_role, workspace_id,
|
||||||
currency, amount_atomic, total_amount_atomic, decimals, membership_included, line_items_json,
|
currency, amount_atomic, total_amount_atomic, decimals, membership_included, line_items_json,
|
||||||
policy_hash, access_class, availability_state, expected_tx_to, expected_tx_data, expected_tx_value_hex, created_at, expires_at, confirmed_at, confirmed_tx_hash
|
policy_hash, access_class, availability_state, expected_tx_to, expected_tx_data, expected_tx_value_hex, created_at, expires_at, confirmed_at, confirmed_tx_hash
|
||||||
FROM marketplace_quotes
|
FROM marketplace_quotes
|
||||||
WHERE quote_id = ?
|
WHERE quote_id = ?
|
||||||
`, strings.TrimSpace(quoteID))
|
`, strings.TrimSpace(quoteID))
|
||||||
var rec marketplaceQuoteRecord
|
var rec marketplaceQuoteRecord
|
||||||
var payerWallet, orgRootID, principalID, principalRole, workspaceID sql.NullString
|
var merchantID, payerWallet, orgRootID, principalID, principalRole, workspaceID sql.NullString
|
||||||
var expectedTxTo, expectedTxData, expectedTxValueHex sql.NullString
|
var expectedTxTo, expectedTxData, expectedTxValueHex sql.NullString
|
||||||
var createdAt, expiresAt, confirmedAt, confirmedTxHash sql.NullString
|
var createdAt, expiresAt, confirmedAt, confirmedTxHash sql.NullString
|
||||||
var membershipIncluded int
|
var membershipIncluded int
|
||||||
err := row.Scan(
|
err := row.Scan(
|
||||||
&rec.QuoteID,
|
&rec.QuoteID,
|
||||||
|
&merchantID,
|
||||||
&rec.Wallet,
|
&rec.Wallet,
|
||||||
&payerWallet,
|
&payerWallet,
|
||||||
&rec.OfferID,
|
&rec.OfferID,
|
||||||
@ -689,6 +701,7 @@ func (s *store) getMarketplaceQuote(ctx context.Context, quoteID string) (market
|
|||||||
}
|
}
|
||||||
return marketplaceQuoteRecord{}, err
|
return marketplaceQuoteRecord{}, err
|
||||||
}
|
}
|
||||||
|
rec.MerchantID = normalizeMerchantID(merchantID.String)
|
||||||
rec.PayerWallet = payerWallet.String
|
rec.PayerWallet = payerWallet.String
|
||||||
rec.OrgRootID = orgRootID.String
|
rec.OrgRootID = orgRootID.String
|
||||||
rec.PrincipalID = principalID.String
|
rec.PrincipalID = principalID.String
|
||||||
@ -726,11 +739,12 @@ func (s *store) getMarketplaceQuoteIDByConfirmedTxHash(ctx context.Context, txHa
|
|||||||
func (s *store) putMarketplaceEntitlement(ctx context.Context, ent marketplaceEntitlementRecord) error {
|
func (s *store) putMarketplaceEntitlement(ctx context.Context, ent marketplaceEntitlementRecord) error {
|
||||||
_, err := s.db.ExecContext(ctx, `
|
_, err := s.db.ExecContext(ctx, `
|
||||||
INSERT INTO marketplace_entitlements (
|
INSERT INTO marketplace_entitlements (
|
||||||
entitlement_id, quote_id, offer_id, wallet, payer_wallet, org_root_id, principal_id, principal_role,
|
entitlement_id, quote_id, merchant_id, offer_id, wallet, payer_wallet, org_root_id, principal_id, principal_role,
|
||||||
workspace_id, state, access_class, availability_state, policy_hash, issued_at, tx_hash
|
workspace_id, state, access_class, availability_state, policy_hash, issued_at, tx_hash
|
||||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
ON CONFLICT(entitlement_id) DO UPDATE SET
|
ON CONFLICT(entitlement_id) DO UPDATE SET
|
||||||
quote_id=excluded.quote_id,
|
quote_id=excluded.quote_id,
|
||||||
|
merchant_id=excluded.merchant_id,
|
||||||
offer_id=excluded.offer_id,
|
offer_id=excluded.offer_id,
|
||||||
wallet=excluded.wallet,
|
wallet=excluded.wallet,
|
||||||
payer_wallet=excluded.payer_wallet,
|
payer_wallet=excluded.payer_wallet,
|
||||||
@ -746,6 +760,7 @@ func (s *store) putMarketplaceEntitlement(ctx context.Context, ent marketplaceEn
|
|||||||
tx_hash=excluded.tx_hash
|
tx_hash=excluded.tx_hash
|
||||||
`, strings.TrimSpace(ent.EntitlementID),
|
`, strings.TrimSpace(ent.EntitlementID),
|
||||||
strings.TrimSpace(ent.QuoteID),
|
strings.TrimSpace(ent.QuoteID),
|
||||||
|
normalizeMerchantID(ent.MerchantID),
|
||||||
strings.TrimSpace(ent.OfferID),
|
strings.TrimSpace(ent.OfferID),
|
||||||
strings.ToLower(strings.TrimSpace(ent.Wallet)),
|
strings.ToLower(strings.TrimSpace(ent.Wallet)),
|
||||||
nullableString(strings.ToLower(strings.TrimSpace(ent.PayerWallet))),
|
nullableString(strings.ToLower(strings.TrimSpace(ent.PayerWallet))),
|
||||||
@ -765,7 +780,7 @@ func (s *store) putMarketplaceEntitlement(ctx context.Context, ent marketplaceEn
|
|||||||
|
|
||||||
func (s *store) getMarketplaceEntitlementByQuote(ctx context.Context, quoteID string) (marketplaceEntitlementRecord, error) {
|
func (s *store) getMarketplaceEntitlementByQuote(ctx context.Context, quoteID string) (marketplaceEntitlementRecord, error) {
|
||||||
row := s.db.QueryRowContext(ctx, `
|
row := s.db.QueryRowContext(ctx, `
|
||||||
SELECT entitlement_id, quote_id, offer_id, wallet, payer_wallet, org_root_id, principal_id, principal_role,
|
SELECT entitlement_id, quote_id, merchant_id, offer_id, wallet, payer_wallet, org_root_id, principal_id, principal_role,
|
||||||
workspace_id, state, access_class, availability_state, policy_hash, issued_at, tx_hash
|
workspace_id, state, access_class, availability_state, policy_hash, issued_at, tx_hash
|
||||||
FROM marketplace_entitlements
|
FROM marketplace_entitlements
|
||||||
WHERE quote_id = ?
|
WHERE quote_id = ?
|
||||||
@ -773,14 +788,20 @@ func (s *store) getMarketplaceEntitlementByQuote(ctx context.Context, quoteID st
|
|||||||
return scanMarketplaceEntitlement(row)
|
return scanMarketplaceEntitlement(row)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *store) listMarketplaceEntitlementsByWallet(ctx context.Context, wallet string) ([]marketplaceEntitlementRecord, error) {
|
func (s *store) listMarketplaceEntitlementsByWallet(ctx context.Context, wallet, merchantID string) ([]marketplaceEntitlementRecord, error) {
|
||||||
rows, err := s.db.QueryContext(ctx, `
|
query := `
|
||||||
SELECT entitlement_id, quote_id, offer_id, wallet, payer_wallet, org_root_id, principal_id, principal_role,
|
SELECT entitlement_id, quote_id, merchant_id, offer_id, wallet, payer_wallet, org_root_id, principal_id, principal_role,
|
||||||
workspace_id, state, access_class, availability_state, policy_hash, issued_at, tx_hash
|
workspace_id, state, access_class, availability_state, policy_hash, issued_at, tx_hash
|
||||||
FROM marketplace_entitlements
|
FROM marketplace_entitlements
|
||||||
WHERE wallet = ?
|
WHERE wallet = ?
|
||||||
ORDER BY issued_at DESC
|
`
|
||||||
`, strings.ToLower(strings.TrimSpace(wallet)))
|
args := []any{strings.ToLower(strings.TrimSpace(wallet))}
|
||||||
|
if strings.TrimSpace(merchantID) != "" {
|
||||||
|
query += " AND merchant_id = ?"
|
||||||
|
args = append(args, normalizeMerchantID(merchantID))
|
||||||
|
}
|
||||||
|
query += " ORDER BY issued_at DESC"
|
||||||
|
rows, err := s.db.QueryContext(ctx, query, args...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -800,8 +821,9 @@ func (s *store) listMarketplaceEntitlementsByWallet(ctx context.Context, wallet
|
|||||||
return records, nil
|
return records, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *store) hasActiveEntitlement(ctx context.Context, wallet, offerID, orgRootID string) (bool, error) {
|
func (s *store) hasActiveEntitlement(ctx context.Context, wallet, merchantID, offerID, orgRootID string) (bool, error) {
|
||||||
wallet = strings.ToLower(strings.TrimSpace(wallet))
|
wallet = strings.ToLower(strings.TrimSpace(wallet))
|
||||||
|
merchantID = normalizeMerchantID(merchantID)
|
||||||
offerID = strings.TrimSpace(offerID)
|
offerID = strings.TrimSpace(offerID)
|
||||||
orgRootID = strings.TrimSpace(orgRootID)
|
orgRootID = strings.TrimSpace(orgRootID)
|
||||||
if wallet == "" || offerID == "" {
|
if wallet == "" || offerID == "" {
|
||||||
@ -812,10 +834,11 @@ func (s *store) hasActiveEntitlement(ctx context.Context, wallet, offerID, orgRo
|
|||||||
SELECT 1
|
SELECT 1
|
||||||
FROM marketplace_entitlements
|
FROM marketplace_entitlements
|
||||||
WHERE wallet = ?
|
WHERE wallet = ?
|
||||||
|
AND merchant_id = ?
|
||||||
AND offer_id = ?
|
AND offer_id = ?
|
||||||
AND state = 'active'
|
AND state = 'active'
|
||||||
`
|
`
|
||||||
args := []any{wallet, offerID}
|
args := []any{wallet, merchantID, offerID}
|
||||||
if orgRootID != "" {
|
if orgRootID != "" {
|
||||||
query += " AND org_root_id = ?"
|
query += " AND org_root_id = ?"
|
||||||
args = append(args, orgRootID)
|
args = append(args, orgRootID)
|
||||||
@ -835,11 +858,12 @@ func (s *store) hasActiveEntitlement(ctx context.Context, wallet, offerID, orgRo
|
|||||||
|
|
||||||
func scanMarketplaceEntitlement(row interface{ Scan(dest ...any) error }) (marketplaceEntitlementRecord, error) {
|
func scanMarketplaceEntitlement(row interface{ Scan(dest ...any) error }) (marketplaceEntitlementRecord, error) {
|
||||||
var rec marketplaceEntitlementRecord
|
var rec marketplaceEntitlementRecord
|
||||||
var payerWallet, orgRootID, principalID, principalRole, workspaceID sql.NullString
|
var merchantID, payerWallet, orgRootID, principalID, principalRole, workspaceID sql.NullString
|
||||||
var issuedAt sql.NullString
|
var issuedAt sql.NullString
|
||||||
err := row.Scan(
|
err := row.Scan(
|
||||||
&rec.EntitlementID,
|
&rec.EntitlementID,
|
||||||
&rec.QuoteID,
|
&rec.QuoteID,
|
||||||
|
&merchantID,
|
||||||
&rec.OfferID,
|
&rec.OfferID,
|
||||||
&rec.Wallet,
|
&rec.Wallet,
|
||||||
&payerWallet,
|
&payerWallet,
|
||||||
@ -860,6 +884,7 @@ func scanMarketplaceEntitlement(row interface{ Scan(dest ...any) error }) (marke
|
|||||||
}
|
}
|
||||||
return marketplaceEntitlementRecord{}, err
|
return marketplaceEntitlementRecord{}, err
|
||||||
}
|
}
|
||||||
|
rec.MerchantID = normalizeMerchantID(merchantID.String)
|
||||||
rec.PayerWallet = payerWallet.String
|
rec.PayerWallet = payerWallet.String
|
||||||
rec.OrgRootID = orgRootID.String
|
rec.OrgRootID = orgRootID.String
|
||||||
rec.PrincipalID = principalID.String
|
rec.PrincipalID = principalID.String
|
||||||
|
|||||||
@ -11,6 +11,13 @@ paths:
|
|||||||
/marketplace/offers:
|
/marketplace/offers:
|
||||||
get:
|
get:
|
||||||
summary: List active offers (launcher/app surface)
|
summary: List active offers (launcher/app surface)
|
||||||
|
parameters:
|
||||||
|
- in: query
|
||||||
|
name: merchant_id
|
||||||
|
required: false
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
description: Merchant namespace. Defaults to `edut.firstparty`.
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
description: Offer list
|
description: Offer list
|
||||||
@ -33,6 +40,12 @@ paths:
|
|||||||
required: true
|
required: true
|
||||||
schema:
|
schema:
|
||||||
type: string
|
type: string
|
||||||
|
- in: query
|
||||||
|
name: merchant_id
|
||||||
|
required: false
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
description: Merchant namespace. Defaults to `edut.firstparty`.
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
description: Offer record
|
description: Offer record
|
||||||
@ -86,6 +99,12 @@ paths:
|
|||||||
schema:
|
schema:
|
||||||
type: string
|
type: string
|
||||||
pattern: '^0x[a-fA-F0-9]{40}$'
|
pattern: '^0x[a-fA-F0-9]{40}$'
|
||||||
|
- in: query
|
||||||
|
name: merchant_id
|
||||||
|
required: false
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
description: Optional merchant filter. If omitted, returns all merchants for wallet.
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
description: Entitlements list
|
description: Entitlements list
|
||||||
@ -113,6 +132,8 @@ components:
|
|||||||
type: object
|
type: object
|
||||||
required: [offer_id, issuer_id, title, status, pricing, policies]
|
required: [offer_id, issuer_id, title, status, pricing, policies]
|
||||||
properties:
|
properties:
|
||||||
|
merchant_id:
|
||||||
|
type: string
|
||||||
offer_id:
|
offer_id:
|
||||||
type: string
|
type: string
|
||||||
issuer_id:
|
issuer_id:
|
||||||
@ -168,6 +189,9 @@ components:
|
|||||||
type: object
|
type: object
|
||||||
required: [wallet, offer_id]
|
required: [wallet, offer_id]
|
||||||
properties:
|
properties:
|
||||||
|
merchant_id:
|
||||||
|
type: string
|
||||||
|
description: Merchant namespace. Defaults to `edut.firstparty`.
|
||||||
wallet:
|
wallet:
|
||||||
type: string
|
type: string
|
||||||
pattern: '^0x[a-fA-F0-9]{40}$'
|
pattern: '^0x[a-fA-F0-9]{40}$'
|
||||||
@ -202,6 +226,8 @@ components:
|
|||||||
properties:
|
properties:
|
||||||
quote_id:
|
quote_id:
|
||||||
type: string
|
type: string
|
||||||
|
merchant_id:
|
||||||
|
type: string
|
||||||
wallet:
|
wallet:
|
||||||
type: string
|
type: string
|
||||||
payer_wallet:
|
payer_wallet:
|
||||||
@ -272,6 +298,9 @@ components:
|
|||||||
properties:
|
properties:
|
||||||
quote_id:
|
quote_id:
|
||||||
type: string
|
type: string
|
||||||
|
merchant_id:
|
||||||
|
type: string
|
||||||
|
description: Merchant namespace. Defaults to `edut.firstparty`.
|
||||||
wallet:
|
wallet:
|
||||||
type: string
|
type: string
|
||||||
pattern: '^0x[a-fA-F0-9]{40}$'
|
pattern: '^0x[a-fA-F0-9]{40}$'
|
||||||
@ -305,6 +334,8 @@ components:
|
|||||||
enum: [entitlement_active]
|
enum: [entitlement_active]
|
||||||
entitlement_id:
|
entitlement_id:
|
||||||
type: string
|
type: string
|
||||||
|
merchant_id:
|
||||||
|
type: string
|
||||||
offer_id:
|
offer_id:
|
||||||
type: string
|
type: string
|
||||||
org_root_id:
|
org_root_id:
|
||||||
@ -335,6 +366,8 @@ components:
|
|||||||
properties:
|
properties:
|
||||||
entitlement_id:
|
entitlement_id:
|
||||||
type: string
|
type: string
|
||||||
|
merchant_id:
|
||||||
|
type: string
|
||||||
offer_id:
|
offer_id:
|
||||||
type: string
|
type: string
|
||||||
wallet_address:
|
wallet_address:
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user