shop cart - BevvyTech/BrewskiDocs GitHub Wiki

Shop Cart & Customer Authentication

Overview

  • Endpoint Group: Public storefront carts & customer auth
  • Purpose: Allow logged-in consumers to manage a cart per brewery storefront, supporting product variants and JWT-based sessions.
  • Availability: Public APIs guarded by the “public” JWT audience; only accessible to authenticated storefront clients.

Endpoints

Method Path Description
POST /public/auth/register Register a marketplace consumer account (email/password).
POST /public/auth/login Authenticate a consumer using email/password.
POST /public/auth/refresh Rotate refresh tokens and issue a new access token.
POST /public/auth/logout Revoke a refresh token.
GET /public/teams/:identifier/cart Fetch (or lazily create) the consumer’s active cart.
POST /public/teams/:identifier/cart/items Add/update a variant or upsell within the cart.
PATCH /public/teams/:identifier/cart/items/:itemId Adjust quantity or swap the variant for an existing line.
DELETE /public/teams/:identifier/cart/items/:itemId Remove an item from the cart.
GET /public/teams/:identifier/products Fetch published storefront products with active variants.
GET /public/teams/:identifier/products/:slug Fetch a single published product (with variants).
GET /public/teams/:identifier/wishlist Fetch the customer’s wishlist for the team.
POST /public/teams/:identifier/wishlist Add a beer to the wishlist ({ "beerId": "<uuid>" }).
DELETE /public/teams/:identifier/wishlist Remove a beer from the wishlist ({ "beerId": "<uuid>" }).
GET /public/marketplace/cart Marketplace cart spanning multiple breweries.
POST /public/marketplace/cart/items Add or replace a line in the marketplace cart ({ "variantId", "quantity" }).
PATCH /public/marketplace/cart/items/:itemId Update quantity or swap variant for a marketplace cart line.
DELETE /public/marketplace/cart/items/:itemId Remove a marketplace cart line.
POST /public/marketplace/cart/guest Persist a lightweight guest basket payload ({ items, token? }) and return a guestToken for future hydrate/merge calls.
POST /public/marketplace/cart/hydrate Hydrate a guest basket payload ({ "items": [...], "guestToken": "<token>" }) and return the computed cart snapshot.
POST /public/marketplace/cart/merge Authenticated bulk merge of the guest basket payload for the signed-in client; accepts the same lightweight items array and/or a guestToken and returns the updated cart.
GET /public/marketplace/wishlist Marketplace wishlist (mixed breweries).
POST /public/marketplace/wishlist Add a beer to the marketplace wishlist ({ "beerId" }).
DELETE /public/marketplace/wishlist Remove a beer from the marketplace wishlist ({ "beerId" }).
GET /public/beers/latest Convenience listing of recently packaged beers (supports limit and packaging_group).

identifier accepts either the team UUID or public slug. Legacy /public/teams/:identifier/* routes still scope a request to a single brewery storefront. Use the new /public/marketplace/* endpoints when customers need to mix beers from multiple breweries—each line item carries its own teamId metadata, and responses echo the same payload for rendering.

All cart routes require an Authorization: Bearer <token> header containing a “public” audience JWT.

Payloads

Registration

POST /public/auth/register
{
  "email": "[email protected]",
  "password": "Passw0rd!",
  "name": "Jordan Black",
  "marketingOptIn": true
}
  • Creates (or reuses) a clients row with login_enabled=true, login_email, hashed password, and is_individual=true. Marketplace accounts are not bound to a single brewery; memberships are attached later when the customer interacts with a team storefront.
  • Response mirrors the backend auth shape:
{
  "client": {
    "id": "...",
    "teamId": null,
    "teamIds": [],
    "name": "Jordan Black",
    "email": "[email protected]"
  },
  "token": {
    "accessToken": "...",
    "tokenType": "Bearer",
    "expiresAt": "2025-06-01T12:00:00.000Z"
  },
  "refresh": {
    "refreshToken": "...",
    "expiresAt": "2025-06-15T12:00:00.000Z"
  }
}
  • teamId remains null until a primary brewery membership exists; teamIds lists all associated breweries (useful for legacy per-team dashboards).

Add Item

POST /public/teams/lantern-brewery/cart/items
{
  "variantId": "1c00bce6-44e7-45c8-8f5f-4a7c33a3f14d",
  "quantity": 2,
  "replaceQuantity": false
}
  • variantId must reference an active storefront_product_variants row belonging to the team and tied to a published product.
  • replaceQuantity=true overwrites the line quantity instead of incrementing.
  • Response returns the recomputed cart totals and snapshot metadata captured at add-time.

To add an upsell accessory instead of a beer variant:

POST /public/teams/lantern-brewery/cart/items
{
  "upsellId": "55dcf0be-53e3-4d2f-9f68-4f6fb1f126aa",
  "quantity": 1
}
  • upsellId references an active record in storefront_upsells. The upsell must have a price_minor defined; the snapshot includes its name, description, coupler metadata, and image URLs.

Update Item

PATCH /public/teams/lantern-brewery/cart/items/95f0...
{
  "quantity": 6,
  "variantId": "3e5c..." // optional swap
}
  • Quantity 0 triggers a delete. When variantId is supplied, the snapshot/price is refreshed from the new variant before totals are recalculated.
  • Upsell lines support quantity updates only—omit variantId when patching and send a payload such as { "quantity": 0 } to remove the accessory.

Cart Response Shape

{
  "cart": {
    "id": "cart-123",
    "teamId": "team-1",
    "clientId": "client-9",
    "status": "active",
    "currency": "GBP",
    "subtotalMinor": 1500,
    "taxTotalMinor": 200,
    "totalMinor": 1700,
    "items": [
      {
        "id": "item-1",
        "kind": "variant",
        "variantId": "variant-1",
        "upsellId": null,
        "quantity": 2,
        "unitPriceMinor": 500,
        "unitTaxRateBps": 2000,
        "lineTotalMinor": 1000,
        "snapshot": {
          "variantId": "variant-1",
          "sku": "440-can",
          "product": {
            "id": "product-1",
            "title": "Lantern IPA",
            "slug": "lantern-ipa",
            "subtitle": "440 ml can"
          }
        }
      },
      {
        "id": "item-2",
        "kind": "upsell",
        "variantId": null,
        "upsellId": "55dcf0be-53e3-4d2f-9f68-4f6fb1f126aa",
        "quantity": 1,
        "unitPriceMinor": 12900,
        "unitTaxRateBps": null,
        "lineTotalMinor": 12900,
        "snapshot": {
          "upsellId": "55dcf0be-53e3-4d2f-9f68-4f6fb1f126aa",
          "slug": "s-type-coupler",
          "name": "S-Type Keg Coupler",
          "description": "Stainless body with check valve.",
          "priceMinor": 12900,
          "currencySymbol": "£",
          "defaultImageUrl": "https://assets.brewskiapp.com/upsells/s-type/main.webp"
        }
      }
    ]
  }
}

Guest Basket Tokens

  • Guest browsers now store only trimmed basket entries in the cookie (kind, id, quantity, optional groupRef) plus the latest guestToken issued by /public/marketplace/cart/guest.
  • Every basket mutation posts the current payload to /public/marketplace/cart/guest. The API persists the payload for 14 days and returns a token string; the frontend stores the token alongside the cookie.
  • /public/marketplace/cart/hydrate accepts either the inline items array, the guestToken, or both. When items is empty but guestToken is present, the server replays the persisted payload and returns the full CartSnapshot.
  • /public/marketplace/cart/merge accepts the same structure. During login the storefront submits { items, guestToken }, and the API merges any persisted guest rows into the authenticated cart before deleting the guest token.
  • Tokens are rotated on every successful /cart/guest request and cleared server-side as soon as a merge completes so stale guest baskets cannot be reused.

Wishlist

  • Wishlist entries are keyed by beerId (per team) and de-duplicate automatically.
  • All wishlist routes require the same “public” JWT auth as the cart endpoints.

Add Item

POST /public/teams/lantern-brewery/wishlist
{
  "beerId": "f778b914-4bf6-4a28-9a91-9bd085249491"
}

Wishlist Response Shape

GET /public/teams/lantern-brewery/wishlist
{
  "wishlist": {
    "id": "wishlist-123",
    "teamId": "d43c046a-10a1-4f52-bd0a-9bf16f828ab7",
    "clientId": "client-9",
    "count": 2,
    "items": [
      {
        "id": "item-1",
        "beerId": "f778b914-4bf6-4a28-9a91-9bd085249491",
        "addedAt": "2025-06-11T10:00:00.000Z",
        "beer": {
          "id": "f778b914-4bf6-4a28-9a91-9bd085249491",
          "name": "Lantern Mosaic IPA",
          "style": "IPA",
          "badgeImageUrl": "https://cdn.brewskiapp.com/assets/beers/lantern-mosaic-ipa.svg",
          "badgeIconUrl": "https://cdn.brewskiapp.com/assets/beers/lantern-mosaic-ipa.svg",
          "team": {
            "id": "d43c046a-10a1-4f52-bd0a-9bf16f828ab7",
            "name": "Lantern Brewery",
            "slug": "lantern-brewery",
            "logoUrl": "https://cdn.brewskiapp.com/assets/logos/lantern-brewery.png"
          }
        }
      }
    ]
  }
}
  • DELETE /public/teams/:identifier/wishlist expects a JSON body { "beerId": "<uuid>" } and returns 204 No Content even when the entry is already absent.

Authentication

  • Access tokens for customers reuse the platform JWT secret and set aud = "public". Marketplace accounts emit tokens without a teamId claim; when a primary membership exists the claim is populated so legacy per-team routes keep working.
  • Refresh tokens live in storefront_refresh_tokens and rotate on each /public/auth/refresh call (same TTL as backend refresh tokens).

Data Model Changes

  • clients table gains login fields (is_individual, login_enabled, login_email, password_hash, last_login_at, marketing_opt_in, public_profile).
  • New enums storefront_product_state and storefront_cart_status.
  • New tables:
    • storefront_products (per-team catalogue wrapper around beers/multipacks).
    • storefront_product_variants (price-bearing variants tied to containers/multipacks).
    • storefront_carts and storefront_cart_items (customer baskets).
    • storefront_refresh_tokens (public refresh token storage).
    • storefront_wishlists and storefront_wishlist_items (per-client, per-team favourite beers).

Notes & Limits

  • Currency currently defaults to GBP; extend once ISO codes arrive in teams.
  • Only published products/active variants are exposed through /public/teams/:identifier/products.
  • Upsell lines appear alongside variants in cart responses with kind: "upsell" and carry upsellId rather than variantId.
  • Inventory reservation is not yet implemented—cart totals are optimistic; downstream checkout should revalidate stock.
  • Team-scoped cart/wishlist endpoints still enforce brewery ownership by comparing the resolved team identifier with the customer’s memberships. Marketplace accounts without a matching membership receive 403 Forbidden responses for those routes and should rely on /public/marketplace/*.

Changelog

Date Author Change
2025-05-31 Agent Added public customer auth and cart endpoints with variation support.
2025-10-21 Agent Added storefront wishlist schema and /public/teams/:identifier/wishlist endpoints.
2025-10-26 Agent Removed teamIdentifier requirement, introduced marketplace auth responses (teamIds) and updated plugin safeguards.
  • GET /public/beers/latest?packaging_group=smallpack restricts results to packaged cans/bottles (use keg or cask for draught-only views).
⚠️ **GitHub.com Fallback** ⚠️