Create sheet_sync service module (DB to Sheet, append-only) #438
Labels
No labels
domain:backend
domain:devops
domain:frontend
status:approved
status:in-progress
status:needs-fix
status:qa
type:bug
type:devops
type:feature
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
ldraney/basketball-api#438
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Type
Feature
Lineage
Standalone — spawned from the westside-sheet-sync project scaffold on 2026-04-10. The core implementation ticket. Depends on the OAuth scope upgrade and the name normalization helper.
Repo
forgejo_admin/basketball-apiUser Story
As head coach Marcus
I want paid jersey orders from the DB to automatically appear in my Google Sheet
So that I don't miss orders and don't have to manually cross-check two systems
Ties to
story:sheet-sync.Context
Tonight we found that Arseni Litvinau paid via Stripe weeks ago but was never added to Marcus's Google Sheet. That's the concrete bug this service fixes: the gap between "paid in the DB" and "visible in Marcus's working doc."
The sheet is
1odx6WSKB5Sg5O3aNzgFoGMR458sA75xb8D5jQJolH8g, single tab "Westside 2", 28 columns. The first row is the header:Player Name | Team | K/Q | Number | Jersey top size | Jersey Short size | Shooter shirt size | Coaches Shirts | Key. The "Coaches Shirts" column actually contains order pipeline status (Placed / Submitted / Invoice Sent / Invoice Paid / shipped / received) — the header label is stale. The sync must NOT rely on header text; it keys off column position.Sync is append-only and one-directional (DB → Sheet). See the sync direction decision in
story-westside-jersey-sheet-sync. Never write to existing rows; never delete rows; only append new rows at the bottom.Idempotency rule: before appending, read the current sheet. For each paid DB player not already in the sheet (by normalized name), append one row. If the same sync runs twice in a row, the second run is a no-op.
File Targets
Files to create:
src/basketball_api/services/sheet_sync.py— containssync_jersey_orders(db: Session) -> SyncResult. SyncResult is a dataclass:{added: list[str], skipped: list[str], errors: list[str]}.src/basketball_api/services/sheets_client.py— thin wrapper aroundrequestscalls tosheets.googleapis.com/v4. Handles token refresh, GET values, POST values:append. Takes a token file path from config.Files to modify:
src/basketball_api/config.py— addsheets_token_path: strandjersey_sheet_id: strsettings, defaulting to reasonable values.Files to create (tests):
tests/test_sheet_sync.py— mocks the sheets_client, asserts that new paid orders get appended, existing players get skipped, and empty DB results in a no-op.tests/test_sheets_client.py— mocksrequests, asserts the HTTP calls have the right shape (URL, headers, body).Files NOT to touch:
src/basketball_api/services/email.py— unrelated.src/basketball_api/routes/*— no routes in this ticket; exposure is via the CronJob ticket, not an HTTP endpoint.Acceptance Criteria
sync_jersey_orders(db)runs with a DB containing Arseni Litvinau (paid) and the sheet not containing him, then his row is appended to the sheet with at least his name, team, and K/Q populated.result.addedis empty (idempotency).jersey_order_status != 'paid', then they are NOT appended regardless of whether they're on the sheet.contract_status = 'declined', then they are NOT appended regardless of jersey status.added,skipped, anderrors.Test Expectations
test_sync_new_paid_order_appended— seed a fake DB with one paid player not in a mock sheet; assert the mock sheet gets one new row.test_sync_idempotent— run the sync twice, assert second call appends zero rows.test_sync_respects_normalization— mock sheet has "LITVINAU, Arseni"; DB has "Arseni Litvinau" paid; assert no duplicate appended.test_sync_skips_unpaid— DB has an unpaid player; assert not appended.test_sync_skips_declined— DB has a declined player; assert not appended.test_sheets_client_appends_to_correct_range— mockrequests, assert the POST URL ishttps://sheets.googleapis.com/v4/spreadsheets/<id>/values/<tab>!A:Z:append?valueInputOption=USER_ENTERED.pytest tests/test_sheet_sync.py tests/test_sheets_client.py -vConstraints
requestsfor HTTP (already in basketball-api's dependencies) — do NOT addgoogle-api-python-client,gspread, or any other SDK.Checklist
Related
westside-sheet-sync— projectstory-westside-jersey-sheet-sync— user story