diff --git a/PROJECT_STATE.md b/PROJECT_STATE.md index f6304a9..7286b2c 100644 --- a/PROJECT_STATE.md +++ b/PROJECT_STATE.md @@ -1,3 +1,42 @@ +## v2.0.5 final invoice workflow - 2026-05-29 UTC + +- Create Invoice now supports a real invoice line description, quantity, unit cost, and optional 13% HST checkbox. +- Invoice subtotal is calculated as quantity × unit cost. +- Tax is calculated server-side only when Apply 13% HST is checked. +- Invoice totals now store subtotal_amount, tax_amount, and total_amount correctly. +- Portal invoice detail now displays Subtotal, HST 13% when applicable, Total Amount, Paid, and Outstanding. +- Invoice item descriptions support long invoice text through invoice_items.description as TEXT. +- Existing invoices are preserved. + +## v2.0.5 portal invoice summary fix - 2026-05-29 UTC + +- Portal invoice detail now selects subtotal_amount and tax_amount from invoices. +- Portal invoice summary now displays Subtotal, HST 13% when tax exists, Total Amount, Paid, and Outstanding correctly. +- Reconciliation refresh path also preserves formatted subtotal and tax values. + +## v2.0.5 follow-up - 2026-05-29 UTC + +- Added Qty to Create Invoice. +- Unit Cost now combines with Qty to calculate invoice line subtotal. +- Portal invoice detail now shows invoice subtotal, HST 13% when present, total amount, paid, and outstanding under the invoice items table. + +## v2.0.5 - 2026-05-29 UTC + +- Updated Create Invoice flow to use Cost / Subtotal plus optional 13% HST checkbox. +- Final Total Amount is now calculated from subtotal + tax server-side. +- Invoice line description is entered as a proper multi-line description and stored in invoice_items. +- Invoice view/print output now shows line amount as subtotal and hides the tax row when tax is zero. +- Preserves existing invoices and uses existing subtotal_amount, tax_amount, and total_amount columns. + +## v2.0.2 - 2026-05-24 19:40 UTC + +- Added Square / Revenue Health panel to /health before crypto balance panels. +- Revenue panel now uses confirmed rows from the payments table, not invoice guesses. +- Shows Square confirmed payment count + CAD total for today, this month, and this year. +- Shows all confirmed payment count + CAD total for today, this month, and this year. +- Added Receivables Aging panel with current, 1-30, 31-60, 61-90, and 90+ day unpaid buckets. +- Renamed health cards from Operations Bal / Treasury Bal to Operations Balance / Treasury Balance. + # PROJECT_STATE - OTB Billing ## v2.0.1 - 2026-05-18 diff --git a/README.md b/README.md index 660c28a..68ce9fc 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,43 @@ -# OTB Billing - v2.0.1 +## v2.0.5 final invoice workflow - 2026-05-29 UTC + +- Create Invoice now supports a real invoice line description, quantity, unit cost, and optional 13% HST checkbox. +- Invoice subtotal is calculated as quantity × unit cost. +- Tax is calculated server-side only when Apply 13% HST is checked. +- Invoice totals now store subtotal_amount, tax_amount, and total_amount correctly. +- Portal invoice detail now displays Subtotal, HST 13% when applicable, Total Amount, Paid, and Outstanding. +- Invoice item descriptions support long invoice text through invoice_items.description as TEXT. +- Existing invoices are preserved. + +## v2.0.5 portal invoice summary fix - 2026-05-29 UTC + +- Portal invoice detail now selects subtotal_amount and tax_amount from invoices. +- Portal invoice summary now displays Subtotal, HST 13% when tax exists, Total Amount, Paid, and Outstanding correctly. +- Reconciliation refresh path also preserves formatted subtotal and tax values. + +## v2.0.5 follow-up - 2026-05-29 UTC + +- Added Qty to Create Invoice. +- Unit Cost now combines with Qty to calculate invoice line subtotal. +- Portal invoice detail now shows invoice subtotal, HST 13% when present, total amount, paid, and outstanding under the invoice items table. + +## v2.0.5 - 2026-05-29 UTC + +- Updated Create Invoice flow to use Cost / Subtotal plus optional 13% HST checkbox. +- Final Total Amount is now calculated from subtotal + tax server-side. +- Invoice line description is entered as a proper multi-line description and stored in invoice_items. +- Invoice view/print output now shows line amount as subtotal and hides the tax row when tax is zero. +- Preserves existing invoices and uses existing subtotal_amount, tax_amount, and total_amount columns. + +## v2.0.2 - 2026-05-24 19:40 UTC + +- Added Square / Revenue Health panel to /health before crypto balance panels. +- Revenue panel now uses confirmed rows from the payments table, not invoice guesses. +- Shows Square confirmed payment count + CAD total for today, this month, and this year. +- Shows all confirmed payment count + CAD total for today, this month, and this year. +- Added Receivables Aging panel with current, 1-30, 31-60, 61-90, and 90+ day unpaid buckets. +- Renamed health cards from Operations Bal / Treasury Bal to Operations Balance / Treasury Balance. + +# OTB Billing - v2.0.2 Build date: 2026-05-18 diff --git a/VERSION b/VERSION index 0ac852d..c2f6de9 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -v2.0.1 +v2.0.5 diff --git a/backend/app.py b/backend/app.py index 88f6af3..fa40946 100644 --- a/backend/app.py +++ b/backend/app.py @@ -341,6 +341,30 @@ def get_invoice_crypto_options(invoice): "blockExplorerUrls": ["https://explorer.ethoprotocol.com"] }, }, + "EGAZ": { + "symbol": "EGAZ", + "chain": "etica", + "label": "EGAZ (Etica Gas)", + "payment_currency": "EGAZ", + "wallet_address": CRYPTO_EVM_PAYMENT_ADDRESS, + "wallet_capable": True, + "asset_type": "native", + "chain_id": 61803, + "decimals": 18, + "token_contract": None, + "rpc_urls": [RPC_ETICA_URL, RPC_ETICA_URL_2], + "chain_add_params": { + "chainId": "0xf16b", + "chainName": "Etica", + "nativeCurrency": { + "name": "Etica Gas", + "symbol": "EGAZ", + "decimals": 18 + }, + "rpcUrls": [RPC_ETICA_URL, RPC_ETICA_URL_2], + "blockExplorerUrls": ["https://explorer.etica-stats.org"] + }, + }, "ETI": { "symbol": "ETI", "chain": "etica", @@ -540,6 +564,15 @@ def get_processing_crypto_option(payment_row): "token_contract": None, "display_amount": amount_text, }, + "EGAZ": { + "symbol": "EGAZ", + "chain": "etica", + "wallet_address": wallet_address, + "asset_type": "native", + "decimals": 18, + "token_contract": None, + "display_amount": amount_text, + }, "ETI": { "symbol": "ETI", "chain": "etica", @@ -550,8 +583,10 @@ def get_processing_crypto_option(payment_row): "display_amount": amount_text, }, } + return mapping.get(currency) + def reconcile_pending_crypto_payment(payment_row): tx_hash = str(payment_row.get("txid") or "").strip() if not tx_hash or not tx_hash.startswith("0x"): @@ -835,6 +870,15 @@ def get_processing_crypto_option(payment_row): "token_contract": None, "display_amount": amount_text, }, + "EGAZ": { + "symbol": "EGAZ", + "chain": "etica", + "wallet_address": wallet_address, + "asset_type": "native", + "decimals": 18, + "token_contract": None, + "display_amount": amount_text, + }, "ETI": { "symbol": "ETI", "chain": "etica", @@ -848,6 +892,7 @@ def get_processing_crypto_option(payment_row): return mapping.get(currency) + def append_payment_note(existing_notes, extra_line): base = (existing_notes or "").rstrip() if not base: @@ -945,6 +990,7 @@ def mark_crypto_payment_confirmed(payment_id, invoice_id, rpc_url): { "ETH": "ethereum", "ETHO": "etho", + "EGAZ": "etica", "ETI": "etica", "USDC": "arbitrum", }.get(str(row.get("payment_currency") or "").upper()), @@ -968,6 +1014,7 @@ def mark_crypto_payment_confirmed(payment_id, invoice_id, rpc_url): ({ "ETH": "ethereum", "ETHO": "etho", + "EGAZ": "etica", "ETI": "etica", "USDC": "arbitrum", }.get(str(row.get("payment_currency") or "").upper())), @@ -3736,6 +3783,34 @@ def invoices(): return render_template("invoices/list.html", invoices=invoices, filters=filters, clients=clients) + +def generate_next_invoice_number(cursor, prefix="INV"): + """ + Generate the next unique invoice number like INV-0012. + Uses the invoices table instead of a hardcoded default. + """ + cursor.execute(""" + SELECT COALESCE( + MAX(CAST(SUBSTRING(invoice_number, 5) AS UNSIGNED)), + 0 + ) AS max_num + FROM invoices + WHERE invoice_number REGEXP '^INV-[0-9]+$' + """) + row = cursor.fetchone() or {} + try: + next_num = int(row.get("max_num") or 0) + 1 + except Exception: + next_num = 1 + + while True: + candidate = f"{prefix}-{next_num:04d}" + cursor.execute("SELECT id FROM invoices WHERE invoice_number = %s LIMIT 1", (candidate,)) + if not cursor.fetchone(): + return candidate + next_num += 1 + + @app.route("/invoices/new", methods=["GET", "POST"]) def new_invoice(): gate = admin_required() @@ -3746,12 +3821,16 @@ def new_invoice(): cursor = conn.cursor(dictionary=True) if request.method == "POST": + from decimal import Decimal, InvalidOperation, ROUND_HALF_UP + client_id = request.form.get("client_id", "").strip() service_id = request.form.get("service_id", "").strip() currency_code = request.form.get("currency_code", "").strip() - total_amount = request.form.get("total_amount", "").strip() + quantity_raw = request.form.get("quantity", "1").strip() + unit_amount_raw = request.form.get("unit_amount", "").strip() due_at = request.form.get("due_at", "").strip() - notes = request.form.get("notes", "").strip() + line_description = request.form.get("description", "").strip() + apply_tax = request.form.get("apply_tax") == "1" errors = [] @@ -3761,18 +3840,41 @@ def new_invoice(): errors.append("Service is required.") if not currency_code: errors.append("Currency is required.") - if not total_amount: - errors.append("Total amount is required.") + if not quantity_raw: + errors.append("Quantity is required.") + if not unit_amount_raw: + errors.append("Unit cost is required.") if not due_at: errors.append("Due date is required.") + if not line_description: + errors.append("Description is required.") + + quantity = None + unit_amount = None + subtotal_amount = None + tax_amount = Decimal("0.00000000") + total_amount = None if not errors: try: - amount_value = float(total_amount) - if amount_value <= 0: - errors.append("Total amount must be greater than zero.") - except ValueError: - errors.append("Total amount must be a valid number.") + quantity = Decimal(quantity_raw).quantize(Decimal("0.0001"), rounding=ROUND_HALF_UP) + if quantity <= 0: + errors.append("Quantity must be greater than zero.") + except (InvalidOperation, ValueError): + errors.append("Quantity must be a valid number.") + + try: + unit_amount = Decimal(unit_amount_raw).quantize(Decimal("0.00000001"), rounding=ROUND_HALF_UP) + if unit_amount <= 0: + errors.append("Unit cost must be greater than zero.") + except (InvalidOperation, ValueError): + errors.append("Unit cost must be a valid number.") + + if not errors: + subtotal_amount = (quantity * unit_amount).quantize(Decimal("0.00000001"), rounding=ROUND_HALF_UP) + if apply_tax: + tax_amount = (subtotal_amount * Decimal("0.13")).quantize(Decimal("0.00000001"), rounding=ROUND_HALF_UP) + total_amount = (subtotal_amount + tax_amount).quantize(Decimal("0.00000001"), rounding=ROUND_HALF_UP) if errors: cursor.execute("SELECT id, client_code, company_name FROM clients ORDER BY company_name") @@ -3787,9 +3889,11 @@ def new_invoice(): "client_id": client_id, "service_id": service_id, "currency_code": currency_code, - "total_amount": total_amount, + "quantity": quantity_raw, + "unit_amount": unit_amount_raw, "due_at": due_at, - "notes": notes, + "description": line_description, + "apply_tax": "1" if apply_tax else "", } return render_template( @@ -3800,23 +3904,15 @@ def new_invoice(): form_data=form_data, ) - invoice_number = generate_invoice_number() - - cursor.execute("SELECT service_name FROM services WHERE id = %s", (service_id,)) - service_row = cursor.fetchone() - service_name = (service_row or {}).get("service_name") or "Service" - - line_description = service_name - if notes: - line_description = f"{service_name} - {notes}" - - oracle_snapshot = fetch_oracle_quote_snapshot(currency_code, total_amount) + oracle_snapshot = fetch_oracle_quote_snapshot(currency_code, str(total_amount)) oracle_snapshot_json = json.dumps(oracle_snapshot, ensure_ascii=False) if oracle_snapshot else None quote_expires_at = normalize_oracle_datetime((oracle_snapshot or {}).get("expires_at")) quote_fiat_amount = total_amount if oracle_snapshot else None quote_fiat_currency = currency_code if oracle_snapshot else None insert_cursor = conn.cursor() + invoice_number = generate_next_invoice_number(insert_cursor) + insert_cursor.execute(""" INSERT INTO invoices ( @@ -3826,6 +3922,7 @@ def new_invoice(): currency_code, total_amount, subtotal_amount, + tax_amount, issued_at, due_at, status, @@ -3835,16 +3932,17 @@ def new_invoice(): quote_expires_at, oracle_snapshot ) - VALUES (%s, %s, %s, %s, %s, %s, UTC_TIMESTAMP(), %s, 'pending', %s, %s, %s, %s, %s) + VALUES (%s, %s, %s, %s, %s, %s, %s, UTC_TIMESTAMP(), %s, 'pending', %s, %s, %s, %s, %s) """, ( client_id, service_id, invoice_number, currency_code, total_amount, - total_amount, + subtotal_amount, + tax_amount, due_at, - notes, + line_description, quote_fiat_amount, quote_fiat_currency, quote_expires_at, @@ -3866,12 +3964,13 @@ def new_invoice(): currency_code, service_id ) - VALUES (%s, 1, 'service', %s, 1.0000, %s, %s, %s, %s) + VALUES (%s, 1, 'service', %s, %s, %s, %s, %s, %s) """, ( invoice_id, line_description, - total_amount, - total_amount, + quantity, + unit_amount, + subtotal_amount, currency_code, service_id )) @@ -3900,7 +3999,6 @@ def new_invoice(): - @app.route("/invoices/pdf/") def invoice_pdf(invoice_id): conn = get_db_connection() @@ -5568,7 +5666,8 @@ def portal_invoice_detail(invoice_id): cursor = conn.cursor(dictionary=True) cursor.execute(""" - SELECT id, client_id, invoice_number, status, created_at, total_amount, amount_paid, + SELECT id, client_id, invoice_number, status, created_at, + subtotal_amount, tax_amount, total_amount, amount_paid, quote_fiat_amount, quote_fiat_currency, quote_expires_at, oracle_snapshot FROM invoices WHERE id = %s AND client_id = %s @@ -5594,6 +5693,8 @@ def portal_invoice_detail(invoice_id): outstanding_raw = to_decimal(invoice.get("total_amount")) - to_decimal(invoice.get("amount_paid")) outstanding = outstanding_raw if outstanding_raw > to_decimal("0") else to_decimal("0") invoice["outstanding"] = _fmt_money(outstanding) + invoice["subtotal_amount"] = _fmt_money(invoice.get("subtotal_amount")) + invoice["tax_amount"] = _fmt_money(invoice.get("tax_amount")) invoice["total_amount"] = _fmt_money(invoice.get("total_amount")) invoice["amount_paid"] = _fmt_money(invoice.get("amount_paid")) invoice["created_at"] = fmt_local(invoice.get("created_at")) @@ -5803,6 +5904,8 @@ def portal_invoice_detail(invoice_id): outstanding_raw = to_decimal(invoice.get("total_amount")) - to_decimal(invoice.get("amount_paid")) outstanding = outstanding_raw if outstanding_raw > to_decimal("0") else to_decimal("0") invoice["outstanding"] = _fmt_money(outstanding) + invoice["subtotal_amount"] = _fmt_money(invoice.get("subtotal_amount")) + invoice["tax_amount"] = _fmt_money(invoice.get("tax_amount")) invoice["total_amount"] = _fmt_money(invoice.get("total_amount")) invoice["amount_paid"] = _fmt_money(invoice.get("amount_paid")) elif reconcile_result.get("state") in {"timeout", "failed_receipt"}: diff --git a/templates/invoices/new.html b/templates/invoices/new.html index c2d46d4..173655c 100644 --- a/templates/invoices/new.html +++ b/templates/invoices/new.html @@ -13,7 +13,6 @@ This invoice number will be generated automatically when the invoice is created.

- {% if errors %}
Please fix the following: @@ -53,7 +52,7 @@ Service *

Currency *
- @@ -62,18 +61,70 @@ Currency *

-Total Amount *
- +Description *
+

-Due Date *
- +Qty *
+ +

+ +

+Unit Cost *
+ +

+ +

+

+
+ Invoice Summary

+ + + + + + + + + + + + + +
Subtotal0.00 CAD
Total Amount0.00 CAD
+
+

-Notes
- +Due Date *
+

@@ -82,6 +133,46 @@ Notes
+ + {% include "footer.html" %} diff --git a/templates/invoices/print_batch.html b/templates/invoices/print_batch.html index aad4937..ac45be7 100644 --- a/templates/invoices/print_batch.html +++ b/templates/invoices/print_batch.html @@ -169,13 +169,13 @@ body { Service Code Service Description - Total + Amount {{ invoice.service_code or '-' }} {{ invoice.service_name or '-' }} {{ invoice.notes or '-' }} - {{ invoice.total_amount|money(invoice.currency_code) }} {{ invoice.currency_code }} + {{ invoice.subtotal_amount|money(invoice.currency_code) }} {{ invoice.currency_code }} @@ -184,10 +184,12 @@ body { Subtotal {{ invoice.subtotal_amount|money(invoice.currency_code) }} {{ invoice.currency_code }} + {% if invoice.tax_amount and invoice.tax_amount|float > 0 %} - {{ settings.tax_label or 'Tax' }} + {{ settings.tax_label or 'HST' }} 13% {{ invoice.tax_amount|money(invoice.currency_code) }} {{ invoice.currency_code }} + {% endif %} Total {{ invoice.total_amount|money(invoice.currency_code) }} {{ invoice.currency_code }} diff --git a/templates/invoices/view.html b/templates/invoices/view.html index e06c405..f36ab56 100644 --- a/templates/invoices/view.html +++ b/templates/invoices/view.html @@ -170,13 +170,13 @@ body { Service Code Service Description - Total + Amount {{ invoice.service_code or '-' }} {{ invoice.service_name or '-' }} {{ invoice.notes or '-' }} - {{ invoice.total_amount|money(invoice.currency_code) }} {{ invoice.currency_code }} + {{ invoice.subtotal_amount|money(invoice.currency_code) }} {{ invoice.currency_code }} @@ -185,10 +185,12 @@ body { Subtotal {{ invoice.subtotal_amount|money(invoice.currency_code) }} {{ invoice.currency_code }} + {% if invoice.tax_amount and invoice.tax_amount|float > 0 %} - {{ settings.tax_label or 'Tax' }} + {{ settings.tax_label or 'HST' }} 13% {{ invoice.tax_amount|money(invoice.currency_code) }} {{ invoice.currency_code }} + {% endif %} Total {{ invoice.total_amount|money(invoice.currency_code) }} {{ invoice.currency_code }} diff --git a/templates/portal_invoice_detail.html b/templates/portal_invoice_detail.html index bd031d1..d6d7b30 100644 --- a/templates/portal_invoice_detail.html +++ b/templates/portal_invoice_detail.html @@ -4,7 +4,7 @@ -{% include "includes/site_nav.html" %}

Invoice Detail

{{ client.company_name or client.contact_name or client.email }}

{% if (invoice.status or "")|lower == "paid" %}
✓ This invoice has been paid. Thank you!
{% endif %} {% if crypto_error %}
{{ crypto_error }}
{% endif %}

Invoice

{{ invoice.invoice_number or ("INV-" ~ invoice.id) }}

Status

{% set s = (invoice.status or "")|lower %} {% if pending_crypto_payment and pending_crypto_payment.txid and not pending_crypto_payment.processing_expired and s != "paid" %} processing {% elif s == "paid" %}{{ invoice.status }} {% elif s == "pending" %}{{ invoice.status }} {% elif s == "overdue" %}{{ invoice.status }} {% else %}{{ invoice.status }}{% endif %}

Created

{{ invoice.created_at }}

Total

{{ invoice.total_amount }}

Paid

{{ invoice.amount_paid }}

Outstanding

{{ invoice.outstanding }}

Invoice Items

{% for item in items %} {% else %} {% endfor %}
DescriptionQtyUnit PriceLine Total
{{ item.description }}{{ item.quantity }}{{ item.unit_price }}{{ item.line_total }}
No invoice line items found.
{% if (invoice.status or "")|lower != "paid" and invoice.outstanding != "0.00" %}

Pay Now

Interac e-Transfer
Send payment to:
payment@outsidethebox.top
Reference: Invoice {{ invoice.invoice_number or ("INV-" ~ invoice.id) }}

Credit Card (Square)

Pay with Credit Card
{% if invoice.oracle_quote and invoice.oracle_quote.quotes and crypto_options %}

Crypto Quote Snapshot

Quoted At: {{ invoice.oracle_quote.quoted_at or "—" }}
Source Status: {{ invoice.oracle_quote.source_status or "—" }}
Frozen Amount: {{ invoice.oracle_quote.amount or invoice.quote_fiat_amount or invoice.total_amount }} {{ invoice.oracle_quote.fiat or invoice.quote_fiat_currency or "CAD" }}
{% if pending_crypto_payment %}
Your quote is protected after acceptance.
{% else %}
Select a crypto asset to accept the quote.
{% endif %}
{% if pending_crypto_payment and pending_crypto_payment.txid %}
--:--
Watching transaction / waiting for confirmation
{% elif pending_crypto_payment %}
--:--
Quote protected while you open wallet
{% else %}
--:--
This price times out:
{% endif %}
{% if pending_crypto_payment and selected_crypto_option %}

{{ selected_crypto_option.label }} Payment Instructions

Send exactly: {{ pending_crypto_payment.payment_amount }} {{ pending_crypto_payment.payment_currency }}
Destination wallet:
{{ pending_crypto_payment.wallet_address }}
Reference / Invoice:
{{ pending_crypto_payment.reference }} {% if selected_crypto_option.wallet_capable and not pending_crypto_payment.txid and not pending_crypto_payment.lock_expired %}
Open in MetaMask Mobile

Fastest way to pay

1. Click Open MetaMask / Rabby if your wallet is installed in this browser.

2. If that does not open your wallet, click Open in MetaMask Mobile.

3. If needed, use Copy Payment Details and send manually.

You do not need to finish everything inside the short quote timer. Once accepted, the quote is protected while you open your wallet.
{% elif pending_crypto_payment.txid %}
Transaction Hash:
{{ pending_crypto_payment.txid }}
Transaction submitted and detected on RPC. Watching transaction / waiting for confirmation.
{% elif pending_crypto_payment.lock_expired %}
price has expired - please refresh your quote to update
{% endif %}
{% else %}
{% for q in crypto_options %} {% endfor %}
AssetQuoted AmountCAD PriceStatusAction
{{ q.label }} {% if q.recommended %}recommended{% endif %} {% if q.wallet_capable %}wallet{% endif %} {{ q.display_amount or "—" }} {% if q.price_cad is not none %}{{ "%.8f"|format(q.price_cad|float) }}{% else %}—{% endif %} {% if q.available %}live{% else %}{{ q.reason or "unavailable" }}{% endif %}
{% endif %}
{% else %}

No crypto quote snapshot is available for this invoice yet.

{% endif %}
{% endif %} {% if invoice_payments %}

Payments Applied

{% for p in invoice_payments %} {% endfor %}
Method Amount Status Received Reference / TXID
{{ p.payment_method_label }} {{ p.payment_amount_display }} {{ p.payment_currency }} {{ p.payment_status }} {{ p.received_at_local }} {% if p.txid %} {{ p.txid }} {% elif p.reference %} {{ p.reference }} {% else %} - {% endif %} {% if p.wallet_address %}
{{ p.wallet_address }}{% endif %}
{% endif %} {% if pdf_url %}
Open Invoice PDF
{% endif %} +{% include "includes/site_nav.html" %}

Invoice Detail

{{ client.company_name or client.contact_name or client.email }}

{% if (invoice.status or "")|lower == "paid" %}
✓ This invoice has been paid. Thank you!
{% endif %} {% if crypto_error %}
{{ crypto_error }}
{% endif %}

Invoice

{{ invoice.invoice_number or ("INV-" ~ invoice.id) }}

Status

{% set s = (invoice.status or "")|lower %} {% if pending_crypto_payment and pending_crypto_payment.txid and not pending_crypto_payment.processing_expired and s != "paid" %} processing {% elif s == "paid" %}{{ invoice.status }} {% elif s == "pending" %}{{ invoice.status }} {% elif s == "overdue" %}{{ invoice.status }} {% else %}{{ invoice.status }}{% endif %}

Created

{{ invoice.created_at }}

Total

{{ invoice.total_amount }}

Paid

{{ invoice.amount_paid }}

Outstanding

{{ invoice.outstanding }}

Invoice Items

{% for item in items %} {% else %} {% endfor %}
DescriptionQtyUnit PriceLine Total
{{ item.description }}{{ item.quantity }}{{ item.unit_price }}{{ item.line_total }}
No invoice line items found.

Invoice Summary

{% if invoice.tax_amount and invoice.tax_amount|float > 0 %}{% endif %}
Subtotal{{ invoice.subtotal_amount }}
HST 13%{{ invoice.tax_amount }}
Total Amount{{ invoice.total_amount }}
Paid{{ invoice.amount_paid }}
Outstanding{{ invoice.outstanding }}
{% if (invoice.status or "")|lower != "paid" and invoice.outstanding != "0.00" %}

Pay Now

Interac e-Transfer
Send payment to:
payment@outsidethebox.top
Reference: Invoice {{ invoice.invoice_number or ("INV-" ~ invoice.id) }}

Credit Card (Square)

Pay with Credit Card
{% if invoice.oracle_quote and invoice.oracle_quote.quotes and crypto_options %}

Crypto Quote Snapshot

Quoted At: {{ invoice.oracle_quote.quoted_at or "—" }}
Source Status: {{ invoice.oracle_quote.source_status or "—" }}
Frozen Amount: {{ invoice.oracle_quote.amount or invoice.quote_fiat_amount or invoice.total_amount }} {{ invoice.oracle_quote.fiat or invoice.quote_fiat_currency or "CAD" }}
{% if pending_crypto_payment %}
Your quote is protected after acceptance.
{% else %}
Select a crypto asset to accept the quote.
{% endif %}
{% if pending_crypto_payment and pending_crypto_payment.txid %}
--:--
Watching transaction / waiting for confirmation
{% elif pending_crypto_payment %}
--:--
Quote protected while you open wallet
{% else %}
--:--
This price times out:
{% endif %}
{% if pending_crypto_payment and selected_crypto_option %}

{{ selected_crypto_option.label }} Payment Instructions

Send exactly: {{ pending_crypto_payment.payment_amount }} {{ pending_crypto_payment.payment_currency }}
Destination wallet:
{{ pending_crypto_payment.wallet_address }}
Reference / Invoice:
{{ pending_crypto_payment.reference }} {% if selected_crypto_option.wallet_capable and not pending_crypto_payment.txid and not pending_crypto_payment.lock_expired %}
Open in MetaMask Mobile

Fastest way to pay

1. Click Open MetaMask / Rabby if your wallet is installed in this browser.

2. If that does not open your wallet, click Open in MetaMask Mobile.

3. If needed, use Copy Payment Details and send manually.

You do not need to finish everything inside the short quote timer. Once accepted, the quote is protected while you open your wallet.
{% elif pending_crypto_payment.txid %}
Transaction Hash:
{{ pending_crypto_payment.txid }}
Transaction submitted and detected on RPC. Watching transaction / waiting for confirmation.
{% elif pending_crypto_payment.lock_expired %}
price has expired - please refresh your quote to update
{% endif %}
{% else %}
{% for q in crypto_options %} {% endfor %}
AssetQuoted AmountCAD PriceStatusAction
{{ q.label }} {% if q.recommended %}recommended{% endif %} {% if q.wallet_capable %}wallet{% endif %} {{ q.display_amount or "—" }} {% if q.price_cad is not none %}{{ "%.8f"|format(q.price_cad|float) }}{% else %}—{% endif %} {% if q.available %}live{% else %}{{ q.reason or "unavailable" }}{% endif %}
{% endif %}
{% else %}

No crypto quote snapshot is available for this invoice yet.

{% endif %}
{% endif %} {% if invoice_payments %}

Payments Applied

{% for p in invoice_payments %} {% endfor %}
Method Amount Status Received Reference / TXID
{{ p.payment_method_label }} {{ p.payment_amount_display }} {{ p.payment_currency }} {{ p.payment_status }} {{ p.received_at_local }} {% if p.txid %} {{ p.txid }} {% elif p.reference %} {{ p.reference }} {% else %} - {% endif %} {% if p.wallet_address %}
{{ p.wallet_address }}{% endif %}
{% endif %} {% if pdf_url %}
Open Invoice PDF
{% endif %}