Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ jobs:
node-version: 20
cache: npm
cache-dependency-path: invofi/apps/frontend/package-lock.json
- run: npm ci
- run: npm ci --legacy-peer-deps
- run: npm run lint
- run: npm run type-check
- run: node ../../../scripts/check-sdk-parity.js
Expand All @@ -41,7 +41,7 @@ jobs:
node-version: 20
cache: npm
cache-dependency-path: invofi/apps/frontend/package-lock.json
- run: npm ci
- run: npm ci --legacy-peer-deps
- run: npm test

frontend-build:
Expand Down Expand Up @@ -70,7 +70,7 @@ jobs:
node-version: 20
cache: npm
cache-dependency-path: invofi/apps/frontend/package-lock.json
- run: npm ci
- run: npm ci --legacy-peer-deps
- run: npm run build

commitlint:
Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,7 @@ invofi/
│ │ ├── invoices/ Create and view invoices
│ │ ├── marketplace/ Lender invoice browser
│ │ │ └── positions/ Secondary-market position listings
│ │ ├── api/documents/ Invoice document upload + content routes (issue #222)
│ │ ├── portfolio/ Lender investment tracker
│ │ ├── profile/ User profile + display name
│ │ └── settings/ Account settings
Expand Down Expand Up @@ -202,6 +203,8 @@ invofi/
│ ├── supabase.ts Auth + database helpers
│ ├── formatters.ts Amount, date, address formatters
│ ├── csv.ts CSV export helpers
│ ├── documents/ Invoice document validation, SHA-256 hash,
│ │ IPFS/Pinata server helpers
│ └── constants.ts Network config, risk tiers, enums
├── scripts/
│ └── close-issues.sh Bulk GitHub issue close
Expand Down
49 changes: 49 additions & 0 deletions docs/06-supabase.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,51 @@ create table position_listings (
updated_at timestamptz default now()
);

-- Invoice proof documents (issue #222) — the access-controlled index to files
-- pinned on IPFS. Bytes live on IPFS; this table stores the content address
-- (CID) plus a SHA-256 hash of the file for tamper detection.
-- Full DDL with policies lives in
-- apps/frontend/src/lib/migrations/002_invoice_documents.sql.
create table invoice_documents (
id uuid primary key default gen_random_uuid(),
invoice_id text not null references invoices(id) on delete cascade,
uploader_id uuid not null references auth.users(id),
file_name text not null,
mime_type text not null
check (mime_type in ('application/pdf', 'image/jpeg', 'image/png')),
file_size integer not null check (file_size > 0 and file_size <= 10485760),
ipfs_cid text not null,
document_hash text not null, -- SHA-256 hex of the file
status text not null default 'pending'
check (status in ('pending', 'verified', 'rejected')),
verification_comment text,
verified_by uuid references auth.users(id),
verified_at timestamptz,
created_at timestamptz not null default now()
);

-- RLS: only invoice parties can read documents, the originator can attach
-- them, and only a lender with an offer on the invoice can verify them.
alter table invoice_documents enable row level security;

create policy "documents_select" on invoice_documents
for select using (
auth.uid() = uploader_id
or exists (select 1 from invoices i where i.id = invoice_documents.invoice_id and i.originator_id = auth.uid())
or exists (select 1 from financing_offers f where f.invoice_id = invoice_documents.invoice_id and f.lender_id = auth.uid())
);
create policy "documents_insert" on invoice_documents
for insert with check (
exists (select 1 from invoices i where i.id = invoice_id and i.originator_id = auth.uid())
);
create policy "documents_verify" on invoice_documents
for update using (
exists (select 1 from financing_offers f where f.invoice_id = invoice_documents.invoice_id and f.lender_id = auth.uid())
);
-- Plus a BEFORE UPDATE trigger (enforce_document_verification_update) that
-- restricts changes to the verification columns and stamps verified_by /
-- verified_at from the caller's session.

-- ── Row Level Security ─────────────────────────────────────────────────────────

alter table user_profiles enable row level security;
Expand Down Expand Up @@ -149,6 +194,9 @@ create index offers_status_idx on financing_offers (status);
create index listings_status_idx on position_listings (status);
create index listings_invoice_id_idx on position_listings (invoice_id);
create index listings_seller_id_idx on position_listings (seller_id);
create index invoice_documents_invoice_id_idx on invoice_documents (invoice_id);
create index invoice_documents_uploader_id_idx on invoice_documents (uploader_id);
create index invoice_documents_status_idx on invoice_documents (status);
```

---
Expand All @@ -173,6 +221,7 @@ Re-enable this before going to mainnet.
| `invoices` | Fast-read invoice list | Soroban contract (authoritative) |
| `financing_offers` | Fast-read offer list | Soroban contract (authoritative) |
| `position_listings` | Secondary-market asks for position tokens (discovery only) | Supabase (authoritative — a listing is an advertisement, not chain state) |
| `invoice_documents` | Invoice proof files (CID + SHA-256 hash + verification state); bytes on IPFS | IPFS/Pinata for bytes; Supabase (authoritative) for the index |

The `invoices` and `financing_offers` tables are display caches. When a user performs an action (register invoice, submit offer, accept, repay), the frontend writes to both the Soroban contract and Supabase simultaneously. If the contract call fails, the Supabase write is skipped.

Expand Down
7 changes: 7 additions & 0 deletions docs/08-environment-variables.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,13 @@ Most environment variables for the InvoFi frontend are prefixed with `NEXT_PUBLI
uses the single `NEXT_PUBLIC_CONTRACT_ID` and routes every call to that one
contract (pre-3-contract deployments keep working).

\** Pinata variables are only needed for the invoice document workflow.

> **Server-only secrets**: `PINATA_API_KEY` and `PINATA_SECRET_API_KEY` are the
> stack's first server-only secrets. They must never be prefixed with
> `NEXT_PUBLIC_` (that would ship them to every browser) and are only read by
> the `app/api/documents/*` route handlers on the Node.js runtime.

---

## Server-Side Secrets
Expand Down
19 changes: 9 additions & 10 deletions invofi/apps/frontend/.env.local.example
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,13 @@ NEXT_PUBLIC_REGISTRY_CONTRACT_ID=
NEXT_PUBLIC_FINANCING_CONTRACT_ID=
NEXT_PUBLIC_REPAYMENT_CONTRACT_ID=

# ── Position tokens (optional; default shown) ────────────────────────────────
NEXT_PUBLIC_POSITION_TOKEN_ASSET=POS:GBDDLOWR6YUEEYUKFKS6ISTCLBQKDPUXAOVJMNJYAACT6UYQGEKYEVZR
# ── Invoice documents / IPFS (issue #222, optional) ────────────────────────
# SERVER-ONLY secrets: read by the app/api/documents/* route handlers on the
# Node.js runtime. NEVER prefix these with NEXT_PUBLIC_ (that would expose them
# to every browser). Required only to upload invoice proof documents.
PINATA_API_KEY=
PINATA_SECRET_API_KEY=

# ── Live portfolio dashboard (optional) ──────────────────────────────────────
# WebSocket relay URL for the live portfolio dashboard (issue #221). When
# empty or unreachable the dashboard degrades to the Soroban event-stream +
# Supabase polling fallback.
NEXT_PUBLIC_WS_URL=

# XLM/USD fallback price used when the live price feed is unreachable.
NEXT_PUBLIC_XLM_USD_PRICE=
# Public IPFS gateway used to fetch document bytes for preview/verification
# (server-only; default https://ipfs.io/ipfs).
IPFS_GATEWAY_URL=
6 changes: 5 additions & 1 deletion invofi/apps/frontend/e2e/fixtures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -229,14 +229,18 @@ export async function mockSupabaseAuth(page: Page): Promise<void> {
/** Stubs the invoice / offer mirror reads the marketplace and detail pages use. */
export async function mockSupabaseMirror(
page: Page,
data: { invoices?: MirrorInvoice[]; offers?: object[] } = {},
data: { invoices?: MirrorInvoice[]; offers?: object[]; documents?: object[] } = {},
): Promise<void> {
await page.route('**/rest/v1/invoices**', (route) =>
route.fulfill({ json: data.invoices ?? SMOKE_INVOICES }),
);
await page.route('**/rest/v1/financing_offers**', (route) =>
route.fulfill({ json: data.offers ?? [] }),
);
// Invoice proof documents (issue #222) — default to none attached.
await page.route('**/rest/v1/invoice_documents**', (route) =>
route.fulfill({ json: data.documents ?? [] }),
);
}

/**
Expand Down
2 changes: 2 additions & 0 deletions invofi/apps/frontend/e2e/invoice-detail.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ test.describe('invoice detail', () => {
await expect(
page.getByRole('heading', { name: /Financing Offers/ }),
).toBeVisible();
// Invoice proof documents section (issue #222).
await expect(page.getByRole('heading', { name: /Documents/ })).toBeVisible();
});

test('renders the on-chain event timeline newest-first', async ({ page }) => {
Expand Down
Loading
Loading