- syncLedgerEntries pulls payment-account ledger entries (chunked by 90d
over required min_created/max_created), classifies debits into fee
categories, and inserts them as idempotent expenses (reference
etsy-ledger-<entry_id>). Amounts are integer minor units -> /100.
- Conservative classifier: only genuine costs become expenses; sale
credits, disbursements, and refunds are skipped. Unclassified debits
are reported back so rules can be refined.
- Folded into POST /api/etsy/sync alongside orders; response includes
ledger result and a legacyEtsyExpenses count (pre-ledger CSV fees).
- DELETE /api/etsy/legacy-fees removes CSV-imported Etsy fee expenses to
avoid double counting; Settings surfaces the count with a one-click
remove, plus a list of skipped/unknown ledger charge types.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
One listing can sell multiple sizes that map to different catalog
products with different costs. Match 'title + variant' before bare
title so size-specific products win, and report unmatched items with
their variant suffix so each size resolves to its own product.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Etsy returns 403 'Shared secret is required in x-api-key header' when the
keystring is used on resource endpoints: the keystring is only the OAuth
client id. Store the shared secret alongside it (Settings form + env
fallback) and send it as x-api-key on users/me and receipt requests.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Callback failures now redirect with the underlying error message, shown
in the Settings toast, instead of a generic failure
- Token endpoint requests use application/x-www-form-urlencoded per
RFC 6749 instead of JSON
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- New EtsySettings model holds the per-user API keystring and callback URL,
managed via GET/PUT /api/etsy/config; env vars remain as optional fallback
- Settings UI gains an API Configuration form (masked saved key, callback URL
prefilled with this origin's /api/etsy/callback); Connect is enabled once
configuration is saved
- OAuth and sync resolve the key per user; post-callback redirect derives
from the stored callback URL origin instead of CLIENT_URL
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- OAuth 2.0 authorization code flow with PKCE; /api/etsy/connect returns the
consent URL, /api/etsy/callback exchanges the code (validated via one-time
state, no JWT) and stores tokens per user with automatic refresh
- /api/etsy/sync pulls all shop receipts and upserts orders by receipt id:
items with SKU/variations, totals, shipping address, tracking, and status,
with catalog costs snapshotted at sync time
- Product matching by exact title/alias first, then etsyListingId
(size-disambiguated); listing ids are learned onto products on first match
- Packing-slip items with cost data are preserved when synced items can't all
be matched
- Settings page: Connect Etsy Shop, Sync Orders Now, Disconnect, and a list
of unmatched item titles after each sync
- Requires ETSY_API_KEY and ETSY_REDIRECT_URI env vars (see .env.example)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>