Agent-first Purchase-to-Pay API built with FastAPI, SQLAlchemy, SQLite, and a Specification-Driven Development workflow.
This repository is a machine-first P2P API for a mid-size building materials distributor. It models the procurement and accounts-payable workflow that an autonomous agent would need to execute safely:
- validate vendors before creating obligations
- create, submit, receive, and inspect purchase orders
- register invoices and run 3-way match controls
- approve invoices and generate balanced GL entries
- pay approved invoices and close the linked purchase order
- monitor vendor exposure and asynchronously flag credit-limit breaches
The goal is not just to expose CRUD endpoints. The goal is to expose a workflow surface that an agent can call deterministically, recover from safely, and inspect without human interpretation.
The project started from a live-coding interview prompt captured in original_assignment.md. The assignment asked for a clean Purchase-to-Pay API that an AI agent could use as a tool-calling interface.
The prompt intentionally emphasized:
- machine-first API design
- financially correct workflow controls
- clear agent recovery signals
- fast local execution with seeded data
Instead of treating the exercise as a one-off coding session, this repository turns the prompt into a traceable, phased implementation with specs, plans, contracts, ADRs, and tests.
The source assignment defined four core responsibilities and three stretch directions:
- purchase-order lifecycle
- invoice registration and 3-way matching
- invoice approval with GL posting
- validation rules around vendor status, receipts, and invoice state
- stretch: vendor exposure query
- stretch: natural-language agent wrapper
- stretch: async credit-limit monitoring
flowchart LR
A[Assignment] --> B[PO]
A --> C[Matching]
A --> D[Approval]
A --> E[Validation]
A --> F[Exposure]
A --> G[Credit Alert]
B --> H[Feature 004]
C --> I[Feature 005]
D --> J[Feature 017]
E --> K[All Features]
F --> L[Feature 001]
G --> M[Feature 019]
This repository uses Specification-Driven Development because the interview problem is easy to overbuild and easy to get subtly wrong.
SDD provides a hard structure for controlling that risk:
spec.mdcaptures user stories, rules, and recovery expectations in machine-usable termsplan.mdturns those requirements into technical decisions and constraintsdata-model.mdandcontracts/make schema and API changes explicit before code lands- ADRs document deliberate deviations from the prompt so they are reviewable
- tests validate the contract against the running service instead of against mocks
That matters here because this API handles procurement commitments, invoice controls, and accounting state. A vague workflow is acceptable in an interview conversation. It is not acceptable in code that claims financial correctness.
flowchart TD
A[Story] --> B[Spec]
B --> C[Plan]
C --> D[Model]
C --> E[Contract]
D --> F[Build]
E --> F
F --> G[Contract Test]
F --> H[Integration Test]
F --> I[ADR]
The repository decomposes the assignment into independently reviewable slices so each business control can be implemented and verified before the next one begins.
| Feature | Scope | Release |
|---|---|---|
001-vendor-management |
Vendor eligibility and vendor AP exposure | v0.1.0 |
004-po-lifecycle |
Draft PO creation, submission, receiving, and PO query | v0.2.0 |
005-invoice-matching |
Invoice registration and 3-way match outcomes | v0.3.0 |
017-invoice-approval-payment |
Invoice approval, GL posting, payment, and PO closure | v0.4.0 |
019-vendor-credit-alert |
Async credit checks, durable query handle, active alerting | v0.5.0 |
timeline
title Feature Delivery Sequence
2026-05-23 : 001 Vendor Management
2026-05-24 : 004 PO Lifecycle
: 005 Invoice Matching
: 017 Invoice Approval and Payment
2026-05-25 : 019 Vendor Credit Alert
For a live MCP end-to-end workflow that exercises every published tool from a VS Code or Claude terminal, and for the prompt/resource discovery surfaces exposed alongside those tools, see docs/mcp-e2e.md.
GET /vendors/{vendor_id}/eligibilityGET /vendors/{vendor_id}/exposure
POST /purchase-ordersPOST /purchase-orders/{purchase_order_id}/submitPOST /purchase-orders/{purchase_order_id}/receiveGET /purchase-orders/{purchase_order_id}
POST /invoicesPOST /invoices/{invoice_id}/matchPOST /invoices/{invoice_id}/approvePOST /invoices/{invoice_id}/payGET /credit-checks/{credit_check_id}
- mutating operations are idempotent
- correlation IDs propagate across requests
- business failures return stable machine-readable codes
- invoice matching distinguishes hard shortfall from non-blocking partial-receipt warning
- async credit evaluation returns a durable
credit_check_idinstead of hiding work in logs
sequenceDiagram
participant Agent
participant API as p2p-api
participant Credit as Credit Check Task
Agent->>API: GET /vendors/V-100/eligibility
API-->>Agent: allowed = true
Agent->>API: POST /purchase-orders
API-->>Agent: DRAFT purchase order
Agent->>API: POST /purchase-orders/{id}/submit
API-->>Agent: SUBMITTED purchase order
Agent->>API: POST /purchase-orders/{id}/receive
API-->>Agent: cumulative receipt progress
Agent->>API: POST /invoices
API-->>Agent: PENDING invoice + credit_check_id
API->>Credit: schedule background exposure check
Agent->>API: POST /invoices/{id}/match
API-->>Agent: MATCHED or shortfall / warning outcome
Agent->>API: POST /invoices/{id}/approve
API-->>Agent: APPROVED invoice + GL entries + credit_check_id
API->>Credit: schedule background exposure check
Credit-->>API: alert created or cleared
Agent->>API: GET /vendors/{id}/exposure
API-->>Agent: outstanding AP + active alert if breached
Agent->>API: POST /invoices/{id}/pay
API-->>Agent: PAID invoice + CLOSED purchase order
The interesting part of this repository is the business behavior, not the framework wiring.
- inactive vendors cannot create new obligations
- goods receipts cannot push cumulative received quantity above ordered quantity
- invoice registration requires a purchase order already submitted for fulfilment
- invoice matching uses received value, not ordered value
- shortfalls return exact uncovered amount and next-step guidance
- partial receipt can still produce a successful match, but only with warning context
- invoices must be
MATCHEDbefore approval andAPPROVEDbefore payment - approval creates exactly two balancing GL entries
- vendor credit checks never block create or approve actions
stateDiagram-v2
[*] --> PENDING
PENDING --> MATCHED: POST /invoices/{id}/match success
PENDING --> PENDING: shortfall or warning-only retry path
MATCHED --> APPROVED: POST /invoices/{id}/approve
APPROVED --> PAID: POST /invoices/{id}/pay
The service follows a strict layered structure so HTTP concerns, business rules, and persistence stay separated.
flowchart TB
Client[Agent / API Client] --> API[FastAPI routes]
API --> Schemas[Pydantic schemas]
API --> Services[Domain services]
Services --> Rules[Domain rules]
Services --> Results[Typed Result and ServiceError]
Services --> Repos[Repository layer]
Repos --> ORM[SQLAlchemy ORM models]
ORM --> DB[(SQLite)]
API --> Middleware[Correlation ID + error handlers]
Services --> Tasks[Background credit check orchestration]
- thin FastAPI route handlers
- service layer returns typed success or failure results
- repositories isolate persistence access
- real SQLite is used in tests instead of mocked persistence
- FastAPI
BackgroundTasksis used as a bounded PoC choice for async credit checks
Feature 019 turns the stretch-goal idea into a queryable workflow.
flowchart TD
A[Invoice create or approve succeeds] --> B[Generate credit_check_id]
B --> C[Persist CreditCheckRecord as PENDING]
C --> D[Return response to caller]
D --> E[Background task computes vendor open AP]
E --> F{Exposure exceeds credit limit?}
F -- Yes --> G[Upsert active vendor CreditAlert]
F -- No --> H[Clear stale active alert]
G --> I[Mark credit check COMPLETED]
H --> I
I --> J[Agent queries credit check status]
I --> K[Agent inspects vendor exposure]
This is one of the clearest examples of the repo's machine-first approach: the workflow is asynchronous, but not opaque.
Several repository choices intentionally go beyond the interview prompt because they make the API more coherent, more deterministic, and easier to defend in review.
The original assignment stopped at approval and GL posting. The repository adds POST /invoices/{id}/pay so the lifecycle can be completed end to end.
The prompt asked for a background task that flags credit-limit breaches. The repository adds persisted CreditCheckRecord state and GET /credit-checks/{id} so an agent can query completion deterministically.
Instead of duplicating invoice-level flags for the same vendor-wide breach, the repo keeps one active alert per vendor and surfaces it via vendor exposure.
Mutating endpoints use idempotency keys and fingerprint validation so retries either replay the original logical success or fail with a stable conflict error when semantics differ.
Approval uses explicit vendor-name-based classification rules and a fallback UNCLASSIFIED_EXPENSE account so approval does not collapse on missing master data.
The original interview prompt inverted the approval journal example. The repository
corrects that by debiting the expense account and crediting AP_CONTROL, while
preserving the rest of the prompt's approval workflow.
On startup, the app seeds a small, useful dataset when the database is empty:
V-100ACME Building Supply, active,NET30, credit limit2000.00V-200Beacon Aggregates, active,NET60, credit limit5000.00V-300Dormant Timber Co, inactive,NET30, credit limit1000.00PO-200inSUBMITTEDstate with one partially received lineGR-200-01receipt showing 60 of 100 units received- seeded invoices across
PENDING,APPROVED,PAID, andMATCHED
That gives you enough state to explore eligibility, receiving, matching, approval, payment, exposure, and credit-risk behaviors immediately.
- Python
3.14+
python -m venv .venv
.\.venv\Scripts\Activate.ps1
pip install -e .[dev]
uvicorn src.main:app --reloadOpen:
- API docs:
http://127.0.0.1:8000/docs - OpenAPI JSON:
http://127.0.0.1:8000/openapi.json
pytest
pytest tests/contract/
pytest tests/integration/
pytest tests/unit/src/
api/ FastAPI routes, schemas, dependencies
core/ settings, typed results, idempotency helpers, errors
domain/ business models, rules, and services
persistence/ SQLAlchemy models, repositories, database, seed data
tests/
contract/ contract-level endpoint validation
integration/ full workflow tests with real persistence
unit/ focused rule and service tests
specs/
001/004/005/017/019 feature specifications, plans, contracts, quickstarts
docs/adr/
architecture decisions for schema and behavioral changes
If you want to understand the project quickly, this order gives the cleanest path:
- original_assignment.md
- specs/001-vendor-management/spec.md
- specs/004-po-lifecycle/spec.md
- specs/005-invoice-matching/spec.md
- specs/017-invoice-approval-payment/spec.md
- specs/019-vendor-credit-alert/spec.md
- CHANGELOG.md
If you are using this repository in an interview or portfolio discussion, the strongest framing is:
- the assignment asked for a machine-first API, so the design optimized for autonomous callers
- the project was decomposed into feature slices to keep financial controls testable
- the repository uses SDD to preserve traceability from assignment language to running code
- enhancements were added only when they improved determinism, observability, or lifecycle completeness
That is the core story this repository tells.