POWorkspaceagentic kit · product owner
← All runs

Client-facing invoice export

Author backlog · innovation-group · po-d1fcf6ec

completedcost $0.9407853Jira: KAN-161, KAN-162, KAN-163, KAN-164, KAN-165, KAN-166, KAN-167, KAN-168, KAN-169
[po-ticket-author running] Pipeline started — https://github.com/N-iX-GenAI-Value-LAB/observability-server/actions/runs/30475080426
[po-ticket-author running] Phase: discovery — restating intent and resolving open decisions
[po-review running] discovery: Restating intent and resolving open decisions for client-facing invoice export
[po-ticket-author running] Phase: story — decomposing into epics and sprint-sized stories
[po-review running] story: Decomposing into epics and sprint-sized user stories with typed Jira fields
[po-ticket-author running] Phase: ac — writing Given/When/Then acceptance criteria for all 7 stories
[po-review running] ac: Writing Given/When/Then acceptance criteria including failure and edge cases
[po-ticket-author running] Linter passed (0 errors, strict). Creating Jira issues in KAN project.
[po-review completed] ac: 31 AC across 7 stories, 2 epics — linter passed strict mode with 0 errors
[po-review completed] Created 9 KAN issues (2 epics, 7 stories) for client-facing invoice export — 31 Given/When/Then AC, linter clean (strict). Keys: KAN-161 through KAN-169.

Backlog

Epic: On-Demand Invoice Export4 stories
  1. Filter and download CSV of client invoices
    • Given I am a logged-in user on the Client Invoice page with filters applied (date range: last 30 days, status: unpaid), When I click the 'Export CSV' button, Then a GET /api/clients/{id}/invoices/export?format=csv request fires with the applied filter params and the browser triggers a file download named invoices_<YYYY-MM-DD>.csv within 5000 ms.
    • Given the CSV download completes, When I open the file in a spreadsheet application, Then the file contains a header row with columns [invoice_id, client_name, date, amount, status, currency] and one data row per invoice matching the applied filters with no extra or missing rows.
    • Given I am a logged-in user with no filters applied on the Client Invoice page, When I click 'Export CSV', Then the downloaded CSV contains all invoices for that client (up to 50,000 rows) and the row count equals the total shown in the invoice list pagination count.
    • Given the export request to GET /api/clients/{id}/invoices/export returns HTTP 500, When the download is attempted, Then a toast notification appears with the text 'Export failed — please try again' and no file download dialog is triggered.
  2. Download XLSX format of filtered client invoices
    • Given I am on the Client Invoice page with invoices listed and filters applied, When I click the 'Export XLSX' button, Then a GET /api/clients/{id}/invoices/export?format=xlsx request fires and the browser downloads a file named invoices_<YYYY-MM-DD>.xlsx within 5000 ms.
    • Given the XLSX file is opened in Excel, Then column A is labeled 'Invoice ID' (string), column B is 'Client Name' (string), column C is 'Date' (Excel date type format YYYY-MM-DD), column D is 'Amount' (numeric 2 decimal places), column E is 'Status' (string), column F is 'Currency' (string) with no mixed-type cell errors reported by Excel.
    • Given the applied filters would produce more than 50,000 matching rows, When I click 'Export XLSX', Then the server returns HTTP 413 and a modal appears with the message 'Your export exceeds 50,000 rows. Please narrow your filters and try again.' and no file download is triggered.
    • Given the export request for XLSX returns HTTP 503, When the download is attempted, Then a toast reads 'Export service unavailable — try again shortly' and the Export XLSX button returns to its enabled state within 500 ms.
  3. Export completes within 5 seconds for up to 50,000 rows
    • Given an invoice dataset of exactly 50,000 rows, When GET /api/clients/{id}/invoices/export?format=csv is called in a pytest benchmark test, Then the server responds with HTTP 200 and the complete response body is received in under 5000 ms as measured from request start to last byte received.
    • Given an invoice dataset of exactly 50,000 rows, When GET /api/clients/{id}/invoices/export?format=xlsx is called in a pytest benchmark test, Then the server responds with HTTP 200 and the complete XLSX response body is received in under 5000 ms as measured from request start to last byte received.
    • Given 5 concurrent export requests each for 10,000 rows, When all requests are fired simultaneously in a pytest concurrency test, Then each individual response completes within 5000 ms and no request returns HTTP 429 or HTTP 503.
    • Given a dataset of exactly 1 row, When GET /api/clients/{id}/invoices/export?format=csv is called, Then the server responds with HTTP 200 with a valid CSV containing 1 header row and 1 data row and response time is under 500 ms.
  4. Export error handling and empty-state feedback
    • Given I am on the Client Invoice page and the device is offline (network request fails), When I click 'Export CSV' or 'Export XLSX', Then the button shows a loading spinner for no more than 3000 ms then reverts to its default label and a toast reads 'Export failed — check your connection and try again.'
    • Given the applied filters produce zero matching invoices and the invoice list shows 'No invoices found', When I hover over or click the 'Export CSV' or 'Export XLSX' button, Then an inline tooltip reads 'No invoices match your current filters.' and no GET /api/clients/{id}/invoices/export request is sent.
    • Given I click Export and the server takes longer than 10,000 ms to respond, When the client-side timeout fires, Then the HTTP request is aborted, the export button reverts to its enabled default state, and a toast reads 'Export timed out — try again with fewer rows.'
    • Given the export completes successfully and the file begins downloading, When the download starts, Then the export button shows a checkmark icon for 2000 ms then reverts to its default label and no error toast appears.
Epic: Scheduled Recurring Invoice Export3 stories
  1. Create a recurring invoice export schedule
    • Given I am logged in as an account manager on the Client Invoice page with filters applied, When I click 'Schedule Export' and complete the form (recipient email pre-filled with my account email, cadence: Weekly, day: Monday, format: CSV), Then POST /api/export-schedules is called with body { client_id, filters, format: 'csv', cadence: 'weekly', day_of_week: 1, recipient_email } and returns HTTP 201 with { id, next_run_at }.
    • Given I am logged in as a non-account-manager user (role != 'account_manager'), When I call POST /api/export-schedules, Then the API returns HTTP 403 with body { error: 'Forbidden: account manager role required' } and no schedule record is created in the database.
    • Given the schedule creation form is submitted with an invalid recipient email (missing '@' character), When the form submit button is clicked, Then a field-level validation error message 'Enter a valid email address' appears below the email field and POST /api/export-schedules is not called.
    • Given a schedule is successfully created with cadence: 'daily', When the system clock reaches next_run_at, Then the export job is triggered within 60 seconds of next_run_at and the schedule record last_run_at is updated and the next next_run_at is set to last_run_at + 86400 seconds.
    • Given I attempt to create a second schedule for the same client with the same filters and cadence, When POST /api/export-schedules is called, Then the API returns HTTP 409 with body { error: 'A schedule with identical settings already exists', existing_id: '<id>' } and no duplicate is created.
  2. View, edit, and delete scheduled exports
    • Given I am logged in as an account manager, When I navigate to 'Scheduled Exports', Then GET /api/export-schedules?owner=me returns HTTP 200 with an array and the page renders one table row per schedule showing [client name, cadence, next_run_at formatted as MMM D YYYY HH:mm, last_run_at or Never, format label].
    • Given I am on the Scheduled Exports list with at least one schedule, When I click 'Edit' on a schedule and change cadence from 'Weekly' to 'Monthly' and click Save, Then PATCH /api/export-schedules/{id} is called with { cadence: 'monthly' } and returns HTTP 200 and the table row updates to display 'Monthly' without a full page reload.
    • Given I am on the Scheduled Exports list, When I click 'Delete' on a schedule and confirm deletion in the modal, Then DELETE /api/export-schedules/{id} returns HTTP 204 and the table row is removed from the DOM within 500 ms and a subsequent GET /api/export-schedules?owner=me does not include the deleted schedule id.
    • Given I attempt to edit a schedule I do not own (owned by another account manager), When PATCH /api/export-schedules/{id} is called, Then the API returns HTTP 403 with { error: 'Forbidden: you do not own this schedule' } and the schedule record is unchanged.
    • Given the Scheduled Exports list is empty (no schedules created yet), When I navigate to the page, Then the table area shows the message 'No scheduled exports yet. Create one from the Client Invoice page.' and no table rows are rendered.
  3. Scheduled export job generates and emails signed download link
    • Given a scheduled export job runs successfully for a 1,000-row invoice dataset, When the job completes, Then an email is sent to the schedule recipient_email with subject 'Your scheduled invoice export is ready' containing a signed download URL and a GET request to that URL within 3600 seconds returns HTTP 200 with Content-Type text/csv or application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.
    • Given a signed download URL that was generated more than 3600 seconds ago, When a GET request is made to that URL, Then the server returns HTTP 410 Gone with body { error: 'Download link has expired. Run the export again from the client page.' }.
    • Given the scheduled export produces more than 50,000 matching rows at run time, When the export job completes, Then the delivered file contains exactly 50,000 rows and the email body includes the note 'This export was capped at 50,000 rows. Narrow your schedule filters to retrieve more specific data.'
    • Given the export job fails due to a database error during invoice retrieval, When the job runner catches the exception, Then the error is logged with structured fields { schedule_id, client_id, error_message, timestamp } at ERROR level and a retry is scheduled within 300 seconds and the recipient is NOT emailed until a successful completion.
    • Given the retry of a failed export job also fails, When the maximum retry count (3 attempts) is reached, Then the schedule owner receives an email with subject 'Scheduled export failed' and body 'Your export schedule could not run after 3 attempts. Please check your settings or contact support.' and the job is marked as failed in the schedule record.