Deployment Change Log d86262740

Showing commits from the last 14 days included in this build (automated version bump commits are excluded).

CommitAuthorDateMessage
d86262740 Tim Richardson 2026-02-27T23:07:27+11:00 feat: Auto-commit by deployer
982fda425 Tim Richardson 2026-02-27T23:06:53+11:00 fix(shopify): prioritize Loop exchange order names in pass processing
0ebf8e5dc Tim Richardson 2026-02-27T22:36:14+11:00 fix(shopify): harden handling-fee money task idempotency checks
9b97b926c Tim Richardson 2026-02-27T22:36:08+11:00 fix(shopify): tighten loop pagination and credit-note repair guards
9058dc9d2 Tim Richardson 2026-02-27T22:36:02+11:00 chore: migrate code review tooling to heavy-review agents
05ddc5ad2 Tim Richardson 2026-02-27T20:29:57+11:00 chore: enhance code-review skill with parallel reviewers
716a465c9 Tim Richardson 2026-02-27T20:29:06+11:00 fix(shopify): clarify pass-one finished-state CN message
1bf2c4fb8 Tim Richardson 2026-02-27T20:10:09+11:00 chore: update code review skill templates
7115c68d8 Tim Richardson 2026-02-27T20:06:08+11:00 fix(core): stop duration timer when websocket closes
6662a9710 Tim Richardson 2026-02-27T20:02:05+11:00 fix(shopify): harden loop pass-one credit note refunds
d3887101f Tim Richardson 2026-02-27T17:00:49+11:00 fix(shopify): remove landing page tutorial video
916096c37 Tim Richardson 2026-02-27T16:33:32+11:00 chore(jaggad): enable loop returns and clarify bg command docs
e8184d912 Tim Richardson 2026-02-27T09:40:21+11:00 fix(integration): ensure Dermapen autofill columns migrate
b94726a25 Tim Richardson 2026-02-27T09:18:07+11:00 feat: improve b2b portal performance monitoring reliability
49f7e4d2d Tim Richardson 2026-02-27T09:14:51+11:00 feat(three_pl_dermapen): add help menu and operator guides
7c2931d32 Tim Richardson 2026-02-26T17:44:49+11:00 feat: Add background run visibility and perf monitor
Improve background orchestration with stricter dispatch validation,
richer lifecycle checkpoints, and a new `bg-list` command so operators
can discover and track detached runs more reliably.

Add a Playwright-based B2B portal performance monitor (script, env
example, tests, and dependency updates) to measure login-to-catalog and
all-orders latency for sales-rep customer flows.

Extend Dermapen autofill queue records with `location` and `notes`
fields, and persist location during queue upserts to improve
observability and audit context for skipped or errored processing.
a0d99ad3c Tim Richardson 2026-02-26T16:55:00+11:00 chore(bg): remove redundant status/tail agents
c4b20eca7 Tim Richardson 2026-02-26T15:23:03+11:00 chore: add quick-start for background deploy usage
f5c9122f9 Tim Richardson 2026-02-26T15:21:23+11:00 chore: document background opencode commands
af127e1b0 Tim Richardson 2026-02-26T14:55:07+11:00 chore: add bg-tail command and background deploy docs
87bbca064 Tim Richardson 2026-02-26T14:38:39+11:00 chore: add background fanout orchestration commands
a054f0b54 Tim Richardson 2026-02-26T12:47:30+11:00 chore: add synthesis agent and worktree override skill
ff68416b9 Tim Richardson 2026-02-26T12:41:24+11:00 feat(shopify): Add OAuth fallback connection check
Fallback to `oauth/access_scopes.json` when configured scopes do not map
to supported REST resources, so connection checks still validate auth
instead of failing with a scope-compatibility error.

Update the OAuth connection view to build `/admin/oauth/...` URLs for
OAuth endpoints and keep versioned `/admin/api/...` URLs for standard
resources. Add tests for fallback resource selection and end-to-end view
behavior.

Also add new `.opencode` review agents (software engineer, sales manager,
accountant) and a `background-deploy` skill to support structured plan
reviews and non-blocking deploy workflows.
2eefc2794 Tim Richardson 2026-02-26T11:03:26+11:00 feat(integration): add DotWMS product sync workflow
d3b415386 Tim Richardson 2026-02-26T11:03:18+11:00 feat(integration): add DearCache LLM query pilot workflow
b5b8fa2af Tim Richardson 2026-02-26T11:03:06+11:00 chore: expand background deployment agent guidance
c0a42d4ad Tim Richardson 2026-02-26T11:03:01+11:00 fix(integration): add Shopify OAuth connection health check
b250e5cc6 Tim Richardson 2026-02-26T11:02:56+11:00 chore: remove stale planning docs
c9dc8b303 Tim Richardson 2026-02-25T17:13:31+11:00 feat(integration): add DearCache object schema catalog
e6e2fbd2f Tim Richardson 2026-02-25T13:56:18+11:00 Merge branch 'feat/shopify-offline-oauth'
607d754cd Tim Richardson 2026-02-25T13:27:45+11:00 chore: add untracked environment files
f80f404d0 Tim Richardson 2026-02-25T13:27:19+11:00 chore: standardize subagent model IDs
5798afeba Tim Richardson 2026-02-25T13:20:51+11:00 fix(integration): open embedded Shopify links in new tabs
52f4b1586 Tim Richardson 2026-02-25T12:48:32+11:00 feat(integration): add embedded Shopify landing page
f5bc26092 Tim Richardson 2026-02-25T12:01:25+11:00 chore: add Shopify app install instructions
207960235 Tim Richardson 2026-02-25T12:01:17+11:00 feat(integration): add Shopify offline OAuth token flow
2fe9f8392 Tim Richardson 2026-02-25T09:29:38+11:00 chore: reformat sale bundle expansion logic
49142bf8d Tim Richardson 2026-02-25T09:29:32+11:00 fix(integration): use OpenCode CLI for commit messages
417de0ed0 Tim Richardson 2026-02-25T09:29:29+11:00 chore: update Serena project metadata
bed707bf1 Tim Richardson 2026-02-24T17:13:21+11:00 chore: Add Serena memory for GLM-5 provider preference
Documents that GLM-5 requests should use the Z.AI Coding Plan
provider with the exact model identifier `zai-coding-plan/glm-5`,
preventing substitution with other GLM variants.
7ffef4496 Tim Richardson 2026-02-23T11:58:13+11:00 chore: add opencode GLM quota plugin dependency
0be6a2399 Tim Richardson 2026-02-23T11:58:09+11:00 fix(integration): simplify opencode headless invocation
b1392a69f Tim Richardson 2026-02-23T11:57:14+11:00 fix(integration): guide Xero tenant recovery workflow
ee5996f01 Tim Richardson 2026-02-22T11:52:55+11:00 fix(integration): reduce duplicate bug tickets in job error analysis
25a8c7267 Tim Richardson 2026-02-22T11:28:49+11:00 chore: update local workspace metadata
2903b5fad Tim Richardson 2026-02-22T11:28:35+11:00 chore: add celery task change playbook guidance
b49562ea0 Tim Richardson 2026-02-22T11:28:29+11:00 chore: refresh opencode agent configs
e80367cff Tim Richardson 2026-02-22T11:25:49+11:00 fix(integration): keep websocket job duration ticking smoothly
c881316b5 Tim Richardson 2026-02-21T16:52:30+11:00 chore: harden deploy agent site parsing rules
59e4f58f7 Tim Richardson 2026-02-21T16:40:18+11:00 fix(integration): add eager-mode job log fallback
44dfce008 Tim Richardson 2026-02-21T16:02:40+11:00 chore: update zoho analytics connector submodule
fc4775597 Tim Richardson 2026-02-21T16:01:58+11:00 chore: persist serena symbol info budget setting
31db34dd9 Tim Richardson 2026-02-20T20:06:50+11:00 fix(integration): parse cached invoice JSON and normalize lookup
98865d8ce Tim Richardson 2026-02-20T18:10:28+11:00 feat(integration): add manual cantontea invoice PDF batch download
03ee19973 Tim Richardson 2026-02-20T12:16:51+11:00 feat: Auto-commit by deployer
c59a0bd75 Tim Richardson 2026-02-20T12:16:41+11:00 fix(cin7_sync): Bypass "recently refreshed" throttle for manual cache updates
Manual cache rebuilds triggered via the UI were silently skipping the orders
refresh when the periodic scheduler had already refreshed within the last 60s.
The user's explicit date parameter was ignored. Now all three object types
(orders, credit notes, payments) bypass the staleness check on manual runs.
937553e34 Tim Richardson 2026-02-20T08:07:27+11:00 chore: Reduce sales cache update progress logging from every 25 to every 100 rows
333bf0158 Tim Richardson 2026-02-20T07:54:08+11:00 fix(status_anxiety): Convert auto_pick_locations from string to list in settings reader [DAS-449]
The auto_pick_locations setting is stored as a comma-separated string via
the CommaSeparatedInput admin widget, but get_status_anxiety_settings_per_dear_entity()
was not converting it to a list (unlike click_and_collect_carriers and email_error_list).
This caused the autopick task to iterate over individual characters instead of location
names, producing 92 repeated errors in production.

Also fixes all legacy ruff E501 (31) and mypy (29) errors in the file:
- Modernize typing imports (Dict→dict, Tuple→tuple, Optional→X|None)
- Fix implicit Optional parameters (PEP 484 no_implicit_optional)
- Add type: ignore[abstract] for DearCachedAPI instantiations
- Add type guards for nullable JSONField access on WebhookTransaction
- Guard against None return from get_product_by_sku in stock_check
- Add explicit return None for missing return path
- Wrap all long lines under 120 chars
f76ff8d36 Tim Richardson 2026-02-20T07:39:45+11:00 feat: Auto-commit by deployer
022d36a75 Tim Richardson 2026-02-20T07:34:59+11:00 fix(core): Use JS form submission for Send Invite button to avoid nested form
The admin change form template's after_related_objects block renders
inside the main <form> tag. HTML forbids nested forms — browsers silently
ignore the inner <form> and submit the outer admin form instead, so the
send-invite URL was never hit.

Replace the nested <form> with a button that dynamically creates and
submits a standalone form via JavaScript, appended to document.body
outside the admin form. Reads the CSRF token from the existing form.
cb13bfef3 Tim Richardson 2026-02-19T23:01:07+11:00 fix(core): Hide invitation inline on the admin Add User form
The UserInvitationInline requires a saved User instance for its FK.
On the two-step Add User form (username+password first), there is no
saved user yet, causing ManagementForm validation errors. Switch from
a static `inlines` attribute to `get_inlines()` that returns the
inline only when editing an existing user (obj is not None).
e977032dd Tim Richardson 2026-02-19T22:52:05+11:00 feat(core): Add email invitation system for admin user management
Add a complete user invitation flow accessible from /admin/auth/user/:

- UserInvitation model (OneToOneField to User) with 256-bit URL-safe tokens,
  status tracking (Pending/Accepted), expiry computation, and resend support
- send_invitation_email() helper using EmailMultiAlternatives for HTML+text
  emails with adaptive subject (new user vs returning user)
- accept_invitation_view for unauthenticated password setup via token link,
  with validation for expired/used/invalid tokens
- CustomUserAdmin replacing default User admin with:
  - "Invite Status" column on user list (Never invited/Pending/Accepted/Expired)
  - Bulk "Send/resend invitation email" action
  - Per-user "Send Invite" button on change form with status display
  - Read-only UserInvitationInline showing send history
- INVITATION_EXPIRY_WEEKS setting (default: 4 weeks)
- HTML/text email templates, set-password form, and error page templates

Resending regenerates the token, immediately invalidating previous links.
After setting password, user is redirected to the login page for 2FA setup.
507da642d Tim Richardson 2026-02-19T21:59:52+11:00 feat: Auto-commit by deployer
72b0ea781 Tim Richardson 2026-02-19T21:59:41+11:00 fix(core): Add lock vanishing diagnostics and protect scheduler greenlets
- Log ERROR with verdict (EXTERNAL DELETION vs LIKELY EXPIRED) when the
  scheduler discovers a lock key has vanished from Valkey, including
  renewal count, time since last renewal, and expire settings
- Log WARNING in stale lock cleanup before deleting, with lock TTL,
  holder job_id, and active hosts list
- Register scheduler and heartbeat greenlets as protected so post-task
  cleanup does not kill lock infrastructure
- Refresh host heartbeat from scheduler loop (every 30s) instead of
  relying on one-shot set at lock acquisition time
91cd239cf Tim Richardson 2026-02-19T20:19:48+11:00 feat: Auto-commit by deployer
91f7244bb Tim Richardson 2026-02-19T20:19:01+11:00 feat(statusanxiety): Add Reset to Defaults button to settings form
Add a reusable reset_url mechanism to DynamicConfigurationView — subclasses
override get_reset_url() to enable a "Reset to Defaults" button on the generic
form template, with a JS confirm dialog before navigating.

For Status Anxiety, the reset view overwrites the KeyValueJson row with the
class-defined default_values and default_schema, then redirects back to the
settings form.

Also updates the form default_values to rename Paddington Store to Claremont
Store (matching the runtime defaults updated in the previous commit).
3c9da2631 Tim Richardson 2026-02-19T20:18:54+11:00 fix(statusanxiety): Rename Paddington Store to Claremont Store in hardcoded defaults
The physical store has been renamed. Update the click_and_collect_carriers
fallback default list and the matching test fixture.
d797fc239 Tim Richardson 2026-02-19T20:12:15+11:00 feat(statusanxiety): Register settings under Starshipit navbar with Configuration menu link
Register StatusAnxiety_FormSettings as a second URL under the starshipit app
namespace at /starshipit/status_anxiety_settings/. The DynamicConfigurationView
auto-detects the app from the URL resolver, so the starshipit-registered URL
renders with the Starshipit navbar instead of the cached_dear navbar.

Add a "Status Anxiety > Settings" entry under the Starshipit Configuration
dropdown menu, gated on SHORT_HOSTNAME == 'statusanxiety'.

Update both settings links in the statusanxiety namespace fragment from
cached_dear:status_anxiety_settings to starshipit:status_anxiety_settings so
they point to the new Starshipit-namespaced URL.

Also wires up the namespace fragment include system for the Starshipit landing
page, loading namespace_fragments/{SHORT_HOSTNAME}.html when present.
4412a9e22 Tim Richardson 2026-02-19T20:11:54+11:00 fix(statusanxiety): Use entity-prefixed key for settings form and add missing config keys
The settings form was writing to KeyValueJson key "status_anxiety_settings" but
runtime code in get_status_anxiety_settings_per_dear_entity() reads from
"{dear_entity}-status_anxiety_settings". This meant any values saved via the form
were invisible to the running autopick, webhook, and fulfilment tasks.

Fix: Override get_initial/get_form_kwargs/form_valid to use entity_setting_key
property that produces the entity-prefixed key matching the runtime reader.

Also adds 4 missing config keys to the form that were previously only available
as hardcoded defaults scattered across the codebase:
- auto_pick_locations (comma-separated list of pick locations)
- NewZealandLocation (Cin7 Core location for NZ fulfilment)
- UnitedStatesLocation (Cin7 Core location for US fulfilment)
- AutoPickHealthCheckURL (healthchecks.io ping URL)

Existing DB rows are seeded with missing keys on first form access via a merge
loop in get_initial(), and the form_schema is kept in sync with the class
definition.
974fb4856 Tim Richardson 2026-02-19T14:59:52+11:00 fix(deploy): Gate dear_analytics entrypoint migration on postgresql_analytics flag
The entrypoint in django-api-sync-base.yaml only checked analytics_db_password
before running `migrate dear_zoho_analytics --database=dear_analytics`, but
setup_new_overlay.py generates analytics_db_password unconditionally for all sites.

Sites without postgresql_analytics enabled (e.g. jaggad) have no dear_analytics
entry in DATABASES, so the migrate command fails with 'invalid choice: dear_analytics'
causing a crash-loop.

Now mirrors the local_settings.py guard: requires BOTH postgresql_analytics AND
analytics_db_password before running dear_analytics migrations.
b938c861d Tim Richardson 2026-02-19T14:52:53+11:00 fix(deploy): Gate analytics DB connection on postgresql_analytics feature flag
The dear_analytics DATABASES entry was guarded only by
analytics_db_password being present, but setup_new_overlay.py generates
this password unconditionally for all sites. Sites without analytics
enabled (postgresql_analytics= empty) would crash on startup trying to
connect to a non-existent database.

Now requires BOTH postgresql_analytics AND analytics_db_password to be
set before adding the dear_analytics database connection.

Fixes jaggad crash-loop: dear_analytics_jaggad role does not exist
because analytics was never enabled for this site.
3405a2ff5 Tim Richardson 2026-02-19T14:39:13+11:00 feat(core): Sort module tiles by ascending priority, Admin always leftmost
Flip Module.ordering from descending to ascending sort_priority so lower
values appear first (leftmost). Admin tile moved before the for-loop in
the template so it is always the first tile regardless of DB state.

Added MinValueValidator(1) on sort_priority — 0 is reserved for the
hardcoded Admin tile. Data migration resets all existing rows to
sort_priority=1 (alphabetical tiebreaker); admins can reassign via
Django admin post-deploy.
e62447460 Tim Richardson 2026-02-19T14:02:25+11:00 feat(deploy): conditional analytics migrations, idempotent overlay setup
- Analytics DB migrations now conditional on password env vars being set
  instead of unconditional || true (django-api-sync-base.yaml)
- setup_new_overlay.py: skip overlay+DB setup if site-secret.values already
  exists, proceed directly to deploy; --pgpassword no longer required for
  existing overlays
2a2dc574f Tim Richardson 2026-02-19T14:02:16+11:00 chore: consolidate code review tooling into /code-review skill
- Enhanced SKILL.md with parallel domain expert agents, verification
  guidelines, and data flow verification checklist
- Removed .opencode/agents/review.md (consolidated into skill)
- Simplified AGENTS.md code review section to reference /code-review skill
18d5986e3 Tim Richardson 2026-02-19T13:58:16+11:00 fix(loop_returns): eval removal, type safety, exception logging, idempotency guards
Security: Replace eval() with ast.literal_eval() in views_loop.py for safe
dict-literal parsing.

Logging: Replace bare print() with logger.debug(); add logger.warning() to
previously silent except-pass block; add logger.exception() to 8 broad
except Exception blocks to capture full stack traces in container logs
alongside existing user-facing job messages.

Type safety: Add type hints to 5 untyped function signatures (process_returns,
pass_one, pass_one_no_exchange, find_exchange_order_name, returns_processing_journal).
Fix 34 legacy mypy errors including: null guards for dear_settings_row and
dear_base_currency, SaleCreditNoteModel narrowing after get_first_auth_credit_note(),
refund variable shadowing (float vs Refund TypedDict), linked_invoice Optional
typing, and values_list None filtering in views.

Idempotency: Wrap refund-paid handling fee post_money_task in try/except API_error
(matching existing Stripe-paid pattern); add debug logging when duplicate handling
fees are detected and skipped.

All files now pass ruff check, ruff format, and mypy with zero errors.
9f07c5cb7 Tim Richardson 2026-02-19T13:58:04+11:00 chore(loop_returns): modernize typing imports, fix lint violations
Replace deprecated typing.List/Dict with builtin list/dict across loop_models,
loop_connector, loop_configurations, and fix_bad_credit_notes. Fix all E501
line-length violations, trailing whitespace, implicit Optional parameters, and
== None comparison (now is None). Pure mechanical changes with zero behavior
impact. All files now pass ruff check and mypy with zero errors.
7c7aff733 Tim Richardson 2026-02-19T13:25:34+11:00 feat(deploy): Add non-interactive deploy mode and wire setup_new_overlay.py end-to-end
tui_deployer.py: Add PlainOutput duck-type class for curses.window so
run_deployment() and wait_for_ingress() work without a terminal UI.
Add --non-interactive flag (skips prompts, uses PlainOutput instead of
curses TUI) and --skip-build flag (skips version bump and Docker
build/push). Non-interactive mode exits 1 on first failure.

reset_django_passwords.py: Add --non-interactive flag that bypasses the
confirmation prompt, proceeding automatically instead of aborting.

setup_new_overlay.py: Add --skip-deploy escape hatch for old behaviour
(overlay + DB only). Without it, main() now chains: invoke_deployment()
→ wait_for_pod_ready() → invoke_superuser_creation(), producing a fully
provisioned site from a single command. Route53 DNS remains a manual
step printed in the completion summary.
1fa9bbd91 Tim Richardson 2026-02-19T11:50:44+11:00 feat(deploy): Generate readonly_main_db_password for jaggad
ca5d441bf Tim Richardson 2026-02-19T11:50:44+11:00 feat: Auto-commit by deployer
324b9371c Tim Richardson 2026-02-18T19:01:53+11:00 feat: Auto-commit by deployer
7dfb22695 Tim Richardson 2026-02-18T18:54:59+11:00 feat: Auto-commit by deployer
f0fa18a71 Tim Richardson 2026-02-18T18:54:45+11:00 fix(three_pl_dermapen): COA attachment dedup broken by Dear URL-encoding filenames
Dear URL-encodes the FileName field when storing attachments (spaces become +,
# becomes %23, etc.). The dedup check compared these URL-encoded names against
plain filenames from SharePoint, so they never matched. This caused every COA
to be re-attached on every 15-minute hexspoor processing cycle.

For sale DPW-25430 this produced 12,061 attachments (43 unique files × ~850
duplicates each over ~9 days of reprocessing).

Fix: decode Dear's FileName with unquote_plus() before comparing, and track
newly-attached names within a single invocation to prevent cross-batch
duplication.
62d4e986b Tim Richardson 2026-02-18T10:38:40+11:00 fix: disambiguate "fulfilment" terminology in emails, logs, and UI to avoid confusion with Cin7 Fulfillment Orders
Error emails and log messages that used "fulfilment" were being misinterpreted by downstream
LLM processing as referring to Cin7 Core's niche "Fulfillment Orders" feature (used with
Amazon FBA / external fulfilment), when they actually refer to the standard Pick/Pack/Ship
workflow. This caused incorrect remediation advice.

Changes across 65 files in starshipit/, three_pl/, three_pl_skyzer/, three_pl_dermapen/,
three_pl_torque/, cached_dear/, and shopify/:

- "fulfilment N" (as an identifier) → "shipment N" (the Ship step number)
- "fulfilment" (general process) → "pick/pack/ship" (the full workflow)
- "Dear" in user-facing strings → "Cin7" (current brand name)
- Emailed error messages now explicitly reference "Pick/Pack/Ship workflow" and
  "Cin7 Core warehouse" to prevent ambiguity
- Variable names, function names, field names, API references, and migrations left unchanged
12a6e293b Tim Richardson 2026-02-18T10:37:31+11:00 fix(three_pl): recover from Dear API duplicate fulfilments instead of always raising (DAS-443)
The Dear API occasionally creates duplicate fulfilments from a single POST request
(observed with 105s response times, suggesting internal retry). Previously, the code
detected and voided duplicates but always raised RuntimeError even when the void
succeeded, causing the order to be skipped until the next autopick cycle.

Now: after voiding duplicates, re-fetch the sale and re-validate. If duplicates are
resolved, continue processing normally. Only raise if duplicates persist after void.

Also improves void_duplicate_fulfillments() with diagnostic WARNING logs when a
duplicate cannot be voided (non-empty or wrong status), and fixes validate_unique_fulfilments()
which previously logged "Duplicate found" unconditionally even when validation passed.

Includes incidental log message renames (fulfilment → pick/pack/ship terminology).
67403f926 Tim Richardson 2026-02-18T09:27:55+11:00 fix(three_pl): guard EDI index creation against missing table
EDIX12TransactionJournal table only exists on entities with EDI
integrations. Wrap CREATE INDEX in a DO block that checks
information_schema.tables first, so the migration succeeds on all
namespaces. Also drops CONCURRENTLY since it cannot run inside DO blocks.
a7e8bec69 Tim Richardson 2026-02-18T09:14:22+11:00 fix: downgrade operational false-positive errors to WARNING across multiple apps
Demote non-actionable log messages from ERROR to WARNING so they no longer
pollute the analyze_job_errors.py bug detection pipeline. These are all
operational/data-quality issues requiring human action on external systems,
not code defects:

- statusanxiety: "Pick is not authorised" in dear_fulfilment_helper and
  ship_fulfilment (same pattern as equipmed fix in 65642accb)
- djcity: Zoho/Dear customer name mismatch requiring manual merge
- vv: Pepperi API rejecting product image uploads (400 "Upload file error")
- 1300tempfence: Zoho account creation failure, lock-not-acquired now uses
  LOCK_NOT_ACQUIRED status (consistent with other lock handlers)
5cc8713f7 Tim Richardson 2026-02-18T09:08:21+11:00 refactor(xero_sync): decompose invoice_sync.py and credit_note_sync.py into focused modules
Extract shared code from two monolithic files (5,925 + 3,753 lines) into
five new modules with clear responsibilities:

- common.py: shared utilities, error classes, tax mapping, lock dates
- contacts.py: Xero contact sync (create, update, find, sync orchestration)
- accounts.py: Xero account management (get, create, ensure)
- line_items.py: unified Dear→Xero line item conversion with include_tax_amount param
- correction.py: void-and-recreate framework (payments, allocations, correction numbers)

Key improvements:
- Eliminates circular dependency between invoice_sync and credit_note_sync
  (deferred imports replaced with direct imports from correction.py)
- Deduplicates get_xero_organization_lock_dates (was copy-pasted in both files)
- Merges 4 line item converters into 2 parameterized functions
- Merges generate_correction_invoice_number + generate_correction_cn_number
- invoice_sync.py reduced from 5,925 to 3,071 lines
- credit_note_sync.py reduced from 3,753 to 3,025 lines
- All consumers updated to import from canonical locations
7003ce797 Tim Richardson 2026-02-18T09:08:09+11:00 fix(cached_dear): use entity name instead of account UUID for DearLocationRenameHistory
DearLocationRenameHistory.dear_instance_id is a CharField expecting the
entity name (e.g. 'orbitkey'), not the Dear account UUID. Using
self.entity instead of dear_account_id fixes rename history tracking.
65642accb Tim Richardson 2026-02-18T09:08:04+11:00 fix(three_pl): downgrade non-actionable log messages from ERROR to WARNING
Operational conditions like multiple draft picks, already-authorised
shipments, un-authorised picks, COA attachment failures, and Hoxton 404s
are not bugs — they are expected scenarios that resolve themselves or
require user action in Dear. Logging them as ERROR pollutes error dashboards.

Also adds explicit RuntimeError handling in task_create_dear_shipments to
catch pick/ship status issues already logged with detail by the caller,
and handles Hoxton/CartonCloud 404 (deleted consignment) gracefully.
2a50e53cf Tim Richardson 2026-02-18T09:07:54+11:00 chore(three_pl): update dependent_product_ids field help text and defaults
AlterField migration for ThreePL_order_fulfilments.dependent_product_ids —
clarifies the help text to document the NULL/empty-list/populated semantics
(NULL = unknown, [] = no dependency, [ids] = stock-dependent).
cc5dedb43 Tim Richardson 2026-02-18T09:07:49+11:00 feat: add performance indexes for WebhookTransaction, JobMaster, JobLog, MiscFileStore, ShopifyInstanceSettings, XeroBankStatement, ThreePL_order_fulfilments, and EDI journal
Non-blocking CONCURRENTLY index creation for core models. Adds composite
indexes on heavily-queried columns: status+source, job+status, job+created,
warehouse_name (case-insensitive), process_status+adoption_attempts, and
more. All migrations use atomic=False for safe production deployment.
8d74be43e Tim Richardson 2026-02-18T08:10:33+11:00 feat(xero_sync): add standalone credit note sync support
Add filter_is_standalone_cn parameter to sync_b2b_sales_modified_since
task and B2BXeroSyncView form, enabling targeted sync of standalone
credit notes (e.g., CR-34162) via the admin UI. When enabled, the task
skips regular sales processing and filters standalone CNs by
string_uniqueID instead of date range.

Also adds find_active_xero_credit_note() to credit_note_sync.py which
walks the void-and-recreate revision chain (-R1, -R2, etc.) to locate
the active non-voided credit note in Xero. Includes formatting cleanup
across credit_note_sync.py (line-length consolidation).

Updates DearCache.string_uniqueID help text to mention credit note
numbers alongside sale order numbers.
db3ba26f9 Tim Richardson 2026-02-18T08:10:24+11:00 chore: restructure AGENTS.md with clearer guiding principles
Expand the Guiding Principles section into distinct subsections for
Question the Premise, Distributed System Concerns, and Verify Data
Structures Before Querying. Streamline build commands and formatting.
48a3804d0 Tim Richardson 2026-02-17T22:39:30+11:00 feat: Auto-commit by deployer
e2e4b33e1 Tim Richardson 2026-02-17T22:33:55+11:00 feat: Auto-commit by deployer
1f640e149 Tim Richardson 2026-02-17T20:50:47+11:00 feat: Auto-commit by deployer
bc7952f17 Tim Richardson 2026-02-17T20:50:37+11:00 fix(xero_sync): Fix standalone credit note already_synced check and fingerprint upgrade
Two bugs fixed:
1. already_synced check used TaskID UUID instead of CN number (e.g. CR-34162)
   as dear_entity_key, causing the duplicate-prevention check to never match.
   Standalone CNs store sync records keyed by CN number, not TaskID.
2. Fingerprint upgrade for standalone CNs queried DearCache with wrong
   object_type (SALE_CREDITS instead of SALE_CREDITS_STAND_ALONE), causing
   all standalone CN fingerprint upgrades to silently fail.
fcc9c8067 Tim Richardson 2026-02-17T18:46:13+11:00 feat: Auto-commit by deployer
d274b2789 Tim Richardson 2026-02-17T18:46:04+11:00 fix(xero_sync): Remove Name/Description from invoice fingerprint lines_hash to eliminate false drift
Invoice drift detection was triggering on 100% of Orbitkey invoices due to Dear
truncating product names inconsistently between API calls. The lines_hash included
Name (for lines) and Description (for charges) alongside financial fields (Account,
Price, Quantity, TaxRule). When Dear returned a slightly different truncation of a
long product name, the hash changed despite zero financial impact.

Investigation of 20 recent drift records confirmed all were LINE_CONTENT_ONLY —
totals, tax, line counts, due dates, currencies all identical. 51 false-positive
DRIFT records were cleared in production.

Changes:
- Exclude Name/Description from lines_hash tuple in both calculate and extract
  functions (commented out with explanation for future re-enablement)
- Bump FINGERPRINT_VERSION to 5 (triggers automatic upgrade on next sync)
- Add DearInstanceSettings lookup patterns to test-harness skill
ba2003684 Tim Richardson 2026-02-16T22:25:34+11:00 fix(cin7_sync): Prevent infinite pagination loop in SizeRanges and Users cache updates
The Cin7 Omni SizeRanges and Users API endpoints do not support pagination
(no page/rows query parameters). When called via yield_next_page(), the API
ignores the page parameter and returns the full dataset on every request,
creating an infinite loop that exhausts all 3 API keys' daily limits (15,000
calls/day) within hours.

This was the root cause of the retrojan healthcheck failure for "RJ Living
Cin7 Order Cache Daily Rebuild" — task_refresh_omni_analytics consumed 14,817
API calls (98.8% of budget) fetching size ranges ~27,000 times.

Both methods now make a single API call and process the complete response
directly, matching how these non-paginated endpoints actually behave.
8d97243ee Tim Richardson 2026-02-16T22:13:22+11:00 fix(three_pl): Process EDI 850 CN (consignment) orders normally
BEG02=CN means "Consignment Order" — a standard order type that should
be auto-processed like OS and DS. The removed code incorrectly treated
CN as a "Change Order" requiring human review, causing valid POs to be
emailed instead of processed. Only one CN order has ever been received
(PO 27493, 2026-01-06) and it was blocked by this code.
ce2b2b2d5 Tim Richardson 2026-02-16T21:50:37+11:00 fix: Harden Django admin across project to prevent OOM and N+1 queries
Project-wide audit found the same class of bug that OOM-killed the
cultiver web pod: ForeignKey fields rendered as <select> dropdowns
loading thousands of records into memory, and N+1 queries in list views.

Critical OOM fixes (raw_id_fields added):
- xysense: Box.order FK to DearCache (50K-200K+ records) had NO admin
  config at all — bare admin.site.register(Box) loaded entire DearCache
  into dropdown. Created proper BoxAdmin and BoxLineAdmin.
- cached_dear: SaleIntercompanyJournalAdmin.dear_sale_guid FK to
  DearCache — same table, same risk.
- shopify: ShopifyPaymentXeroTransactionAdmin had two unprotected FK
  dropdowns (shopify_payout, xero_bank_statement). ShopifyPaymentXeroPayout
  also missing raw_id_fields for xero_bank_statement.
- dear_purchasing: Both WarehouseReceiptBatchPart and CreatedPurchaseReceipt
  had receipt_batch FK without raw_id_fields.

N+1 query fixes (list_select_related added):
- core: JobLogAdmin (job FK in list_display)
- cached_dear: DearCacheAdmin (dear_account_id FK in list_display)
- cached_dear/xero_sync: XeroSyncRecordAdmin, DearCustomerXeroContactMapping,
  XeroSyncSettingsAdmin (dear_instance FK in list_display)
- shopify: ShopifyLocationMappingAdmin (shopify_instance_settings FK)
- revel_pos: RevelOrderImportLogAdmin (establishment FK)

Also fixed: removed FK object references from search_fields where invalid,
removed duplicate list_filter entries, removed JSONField from list_display
in dear_purchasing.
1dfb9881b Tim Richardson 2026-02-16T21:44:36+11:00 fix(three_pl): Prevent OOM in Django admin for EDIX12 models
EDIX12TransactionJournal and EDIX12FunctionalGroup admin change forms
loaded all 15,377 EDIX12Interchange records (~10KB serialized_content each)
into memory via FK select dropdowns, consuming ~155MB per dropdown per page
load. Over time this accumulated past the 2GB pod memory limit, causing
OOMKilled crashes.

Changes:
- Add raw_id_fields for all FK fields on TransactionJournal and FunctionalGroup
  admins, replacing memory-heavy <select> dropdowns with ID input + lookup
- Add readonly_fields for large TextFields (serialized_content, edi_transaction_string)
- Use interchange_id in list_display to avoid N+1 queries on list views
- Remove duplicate transaction_set_identifier_code from TransactionJournal list_filter
a02eec856 Tim Richardson 2026-02-16T12:24:28+11:00 chore(dear_zoho_analytics): Remove unused Shippit integration
Shippit webhook, task scheduling, CSV processing, and analytics model were only
configured for CultiverAus and are no longer needed. Removes the module, model,
Celery tasks, view, URL pattern, and generates a migration to drop the
shippit_transactions table.
683c53955 Tim Richardson 2026-02-16T11:44:41+11:00 fix(interentity): Include expected format example in stock transfer location error messages
When a stock transfer location like 'Melbourne Discrepancy' lacks the required
RI/RH prefix, the error now shows the expected format (e.g. 'RH Melbourne Discrepancy')
so the user knows exactly how to fix the location name in Dear.
491cd5d2f Tim Richardson 2026-02-16T09:32:32+11:00 fix(three_pl): Keep process status popover open while hovering over its content
Switch Bootstrap popover trigger from "hover focus" to "manual" with a JS-managed
hover bridge: a 150ms delayed hide is cancelled if the mouse enters the popover body.
This lets users reach interactive elements like the "View & Adopt" button inside the
popover without it disappearing on mouseout from the trigger element.
86bd73b2c Tim Richardson 2026-02-16T08:55:35+11:00 fix(dear_zoho_analytics): Consolidate currency string parsing to prevent ValueError on Zoho numeric fields (DAS-440)
Zoho Analytics SQL export can return currency-formatted strings (e.g. '$2.03')
for numeric columns. The conversion rate lookup in ZohoBackendDear.get_conversion_rate()
used raw float() which crashed on aurora-lites. Previous fixes (DAS-428, DAS-436) only
covered Dear API supplier prices and AverageCost fields but missed Zoho query results.

Promoted _parse_currency_value() from dear_analytics_logic_sales_tables to a shared
parse_currency_value() in common_definitions.py. Applied it to all raw float() calls
on Zoho query results across zoho_backend_dear.py (conversion rates, net movements),
cogs_timing.py (excess revenue/COGS values), and zoho_helpers.py (revenue quantities,
unit net — replacing a fragile [1:] slice hack). Removed duplicate nested definition
from zoho_helpers.get_exchange_rate().
12219fd1e Tim Richardson 2026-02-15T13:24:51+11:00 fix(dear_zoho_analytics): Validate TypedDict Literals and model choices against djcity production data
Queried 840K orders, 21K products, 13K purchases, 28K stock transfers from
djcity DearCache to cross-check every Literal type alias and choices list
against real Dear API values.

Critical fix: FulFilmentStatus used underscores (PARTIALLY_FULFILLED) but
Dear API returns spaces (PARTIALLY FULFILLED). This caused a silent bug in
dear_analytics_logic_sales_tables.py:799 where the fulfilment status check
never matched, potentially excluding partially-fulfilled orders from BI.

TypedDict changes:
- FulFilmentStatus: underscores to spaces in SaleListRow and DearSale
- Sale Status: add CLOSED
- StockTransferStatus: add ORDERED, PICKING
- PurchaseGlobalStatus: add 4 compound statuses (RECEIVING / CREDITED,
  RECEIVED / CREDITED, PARTIALLY INVOICED, COMPLETED / CREDIT NOTE CLOSED)
- CombinedPaymentStatus: add OVERPAID, OVERPAID / CREDITED
- CombinedInvoiceStatus: add PARTIALLY INVOICED / CREDITED
- ProductMovementType: add Assembly, Assembly Cost Change, Cost Adjustment,
  Production Run (discovered values, not API-documented)
- Purchase CombinedInvoiceStatus/CombinedPaymentStatus: typed from bare str
  to Optional[Literal[...]] using production data

Model choices updated to match. Migration 0073 covers 16 field alterations.
88d936096 Tim Richardson 2026-02-15T13:24:34+11:00 fix(cached_dear): Harden Xero void/credit-note exception handling (DAS-438)
Replace broad Exception catches with specific Xero SDK exception types
(AccountingBadRequestException, HTTPStatusException, RateLimitException).
Add _format_http_status_error() helper for structured diagnostic logging
of Xero API errors including HTTP status, reason, and truncated body.

Reformat imports and string literals for ruff compliance.
b5c2c499a Tim Richardson 2026-02-15T11:57:43+11:00 Merge branch 'feature/analytics-model-choices'
34b4d11b0 Tim Richardson 2026-02-15T11:57:12+11:00 feat(dear_zoho_analytics): Add choices metadata to analytics models and consolidate schema extraction
Add choices= metadata to all analytics model fields with enumerated values
(sale status, fulfilment status, payment status, stock status, etc.) sourced
from Dear API TypedDicts and API reference documentation. These are metadata-only
constraints (no DB enforcement) that enable schema introspection for both the
MCP server and the HTML documentation page.

Consolidate duplicated schema extraction logic into a single source of truth at
dear_zoho_analytics/schema_utils.py. The MCP server's schema_extractor.py becomes
a thin re-export shim for backwards compatibility. The AnalyticsTableDocumentationView
now uses the shared get_table_schema() API instead of inline field iteration, ensuring
any future metadata additions (e.g. db_index, validators) are automatically reflected
in both the MCP server and the documentation page.

Add blank and unique fields to FieldSchema TypedDict for complete field introspection.
2c47d601e Tim Richardson 2026-02-14T16:04:39+11:00 fix(dear_zoho_analytics): Harden views security and reformat analytics logic
views.py security fixes:
- Use hmac.compare_digest for timing-safe token comparison (shippit webhook, zoho proxy)
- Remove hardcoded fallback password from zoho_proxy_password lookup
- Require configured shippit_webhook_token, return 503 if missing
- Add @login_required to ShowTableDefs and zoho_table_update_timestamps
- Prevent XSS in error responses: replace <pre> HTML with content_type="text/plain"
- Remove duplicate exception handler in verify_product_availability_integrity_view

dear_analytics_logic.py:
- Ruff formatting (line wrapping, whitespace)
- Widen summarise_lines_by_product_id param to Sequence for type covariance
fde8eb0e8 Tim Richardson 2026-02-14T15:59:56+11:00 fix(dear_zoho_analytics): Resolve all 269 mypy errors in dear_analytics_logic_sales_tables.py
Fix long-standing mypy type errors that accumulated when DearSale and related TypedDicts
were introduced without updating existing code. Changes are type-annotation-only with no
logic modifications.

Key fixes:
- Annotate 19 dict construction sites (row, header, new_row, totals_per_doc, etc.) as
  dict[str, Any] to prevent mypy from inferring overly-narrow types from first assignment
- Modernize legacy "# type:" comments to inline annotations (set[str], list[dict])
- Widen consolidate_service_lines and summarise_lines_by_product_id params from list[T]
  to Sequence[T] to fix list covariance errors
- Change prepare_analytics_sales_order_payments and prepare_analytics_sales_line_lifecycle
  param types from dict to DearSale
- Cast dynamic TypedDict key access (f"AdditionalAttribute{i}") through Any intermediates
- Convert TypedDict mutation in payments prep to dict() copies instead of in-place mutation
- Annotate float initializers (revenue_qty_total, restock_qty, max_cost_price_base_cur)
- Remove redundant type annotations on redefined variables (new_row, row, uniq_key)
- 5 targeted # type: ignore comments for genuine mypy limitations (nonlocal binding,
  Django model field assignment, structurally-compatible TypedDict args)
da7cbd514 Tim Richardson 2026-02-14T15:06:51+11:00 fix(cin7_sync): Use defensive Id access in Cin7 cache update methods (DAS-437)
The Cin7 Omni API can return records without an 'Id' field, causing KeyError
crashes in update_size_ranges, update_production_jobs, update_bom_masters, and
update_serial_numbers. This crashed the entire analytics refresh task since
KeyError is not caught by the API_error exception handler.

Applied the same defensive .get('Id') + skip pattern already used by
update_users. Skipped records are counted and logged for diagnostics.