@ -26,6 +26,7 @@
border-radius: 14px;
border-radius: 14px;
padding: 1rem;
padding: 1rem;
background: rgba(255,255,255,0.03);
background: rgba(255,255,255,0.03);
margin-bottom: 1rem;
}
}
.detail-card h3 {
.detail-card h3 {
margin-top: 0;
margin-top: 0;
@ -45,13 +46,6 @@
background: #e9eef7;
background: #e9eef7;
color: #10203f;
color: #10203f;
}
}
.invoice-actions {
margin-top: 1rem;
}
.invoice-actions a {
margin-right: 1rem;
text-decoration: underline;
}
.status-badge {
.status-badge {
display: inline-block;
display: inline-block;
padding: 0.18rem 0.55rem;
padding: 0.18rem 0.55rem;
@ -75,6 +69,108 @@
background: rgba(148, 163, 184, 0.20);
background: rgba(148, 163, 184, 0.20);
color: #cbd5e1;
color: #cbd5e1;
}
}
.pay-card {
border: 1px solid rgba(255,255,255,0.16);
border-radius: 14px;
padding: 1rem;
background: rgba(255,255,255,0.03);
margin-top: 1.25rem;
}
.pay-selector-row {
display:flex;
gap:0.75rem;
align-items:center;
flex-wrap:wrap;
margin-top:0.75rem;
}
.pay-selector {
padding: 10px 12px;
min-width: 220px;
border-radius: 8px;
}
.pay-panel {
margin-top: 1rem;
padding: 1rem;
border: 1px solid rgba(255,255,255,0.12);
border-radius: 12px;
background: rgba(255,255,255,0.02);
}
.pay-panel.hidden {
display: none;
}
.pay-btn {
display:inline-block;
padding:12px 18px;
color:#ffffff;
text-decoration:none;
border-radius:8px;
font-weight:700;
border:none;
cursor:pointer;
margin:8px 0 0 0;
}
.pay-btn-square {
background:#16a34a;
}
.pay-btn-crypto {
background:#2563eb;
}
.error-box {
border: 1px solid rgba(239, 68, 68, 0.55);
background: rgba(127, 29, 29, 0.22);
color: #fecaca;
border-radius: 10px;
padding: 12px 14px;
margin-bottom: 1rem;
}
.snapshot-wrap {
position: relative;
margin-top: 1rem;
border: 1px solid rgba(255,255,255,0.14);
border-radius: 14px;
padding: 1rem;
background: rgba(255,255,255,0.02);
}
.snapshot-header {
display:flex;
justify-content:space-between;
gap:1rem;
align-items:flex-start;
}
.snapshot-meta {
flex: 1 1 auto;
min-width: 0;
line-height: 1.65;
}
.snapshot-timer-box {
width: 220px;
min-height: 132px;
border: 1px solid rgba(255,255,255,0.16);
border-radius: 14px;
background: rgba(0,0,0,0.18);
display:flex;
flex-direction:column;
justify-content:center;
align-items:center;
text-align:center;
padding: 0.9rem;
}
.snapshot-timer-value {
font-size: 2rem;
font-weight: 800;
line-height: 1.1;
}
.snapshot-timer-label {
margin-top: 0.55rem;
font-size: 0.95rem;
opacity: 0.95;
}
.snapshot-timer-expired {
color: #f87171;
}
.quote-table {
.quote-table {
width: 100%;
width: 100%;
border-collapse: collapse;
border-collapse: collapse;
@ -84,16 +180,12 @@
padding: 0.75rem;
padding: 0.75rem;
border-bottom: 1px solid rgba(255,255,255,0.12);
border-bottom: 1px solid rgba(255,255,255,0.12);
text-align: left;
text-align: left;
vertical-align: top;
}
}
.quote-table th {
.quote-table th {
background: #e9eef7;
background: #e9eef7;
color: #10203f;
color: #10203f;
}
}
.quote-meta {
font-size: 0.95rem;
line-height: 1.6;
opacity: 0.95;
}
.quote-badge {
.quote-badge {
display: inline-block;
display: inline-block;
padding: 0.14rem 0.48rem;
padding: 0.14rem 0.48rem;
@ -110,6 +202,58 @@
background: rgba(239, 68, 68, 0.18);
background: rgba(239, 68, 68, 0.18);
color: #f87171;
color: #f87171;
}
}
.quote-pick-btn {
padding: 8px 12px;
border-radius: 8px;
border: none;
background: #2563eb;
color: #fff;
font-weight: 700;
cursor: pointer;
}
.quote-pick-btn[disabled] {
opacity: 0.5;
cursor: not-allowed;
}
.lock-box {
margin-top: 1rem;
border: 1px solid rgba(34, 197, 94, 0.28);
background: rgba(22, 101, 52, 0.16);
border-radius: 12px;
padding: 1rem;
}
.lock-box.expired {
border-color: rgba(239, 68, 68, 0.55);
background: rgba(127, 29, 29, 0.22);
}
.lock-grid {
display:grid;
grid-template-columns: 1fr 220px;
gap:1rem;
align-items:start;
}
.lock-code {
display:block;
margin-top:0.35rem;
padding:0.65rem 0.8rem;
background: rgba(0,0,0,0.22);
border-radius: 8px;
overflow-wrap:anywhere;
}
@media (max-width: 820px) {
.snapshot-header,
.lock-grid {
grid-template-columns: 1fr;
display:block;
}
.snapshot-timer-box {
width: 100%;
margin-top: 1rem;
min-height: 110px;
}
}
< / style >
< / style >
< link rel = "icon" type = "image/png" href = "/static/favicon.png" >
< link rel = "icon" type = "image/png" href = "/static/favicon.png" >
< / head >
< / head >
@ -128,13 +272,16 @@
< / div >
< / div >
< / div >
< / div >
{% if (invoice.status or "")|lower == "paid" %}
{% if (invoice.status or "")|lower == "paid" %}
< div style = "background:#166534;color:#ecfdf5;padding:12px 14px;margin:0 0 20px 0;border-radius:8px;font-weight:600;border:1px solid rgba(255,255,255,0.12);" >
< div style = "background:#166534;color:#ecfdf5;padding:12px 14px;margin:0 0 20px 0;border-radius:8px;font-weight:600;border:1px solid rgba(255,255,255,0.12);" >
✓ This invoice has been paid. Thank you!
✓ This invoice has been paid. Thank you!
< / div >
< / div >
{% endif %}
{% endif %}
{% if crypto_error %}
< div class = "error-box" > {{ crypto_error }}< / div >
{% endif %}
< div class = "detail-grid" >
< div class = "detail-grid" >
< div class = "detail-card" >
< div class = "detail-card" >
< h3 > Invoice< / h3 >
< h3 > Invoice< / h3 >
@ -197,86 +344,242 @@
< / tbody >
< / tbody >
< / table >
< / table >
{% if (invoice.status or "")|lower != "paid" and invoice.outstanding != "0.00" %}
< div class = "pay-card" >
< h3 > Pay Now< / h3 >
< div class = "pay-selector-row" >
< label for = "payMethodSelect" > < strong > Choose payment method:< / strong > < / label >
< select id = "payMethodSelect" class = "pay-selector" >
< option value = "" { % if not pay_mode % } selected { % endif % } > Select…< / option >
< option value = "etransfer" { % if pay_mode = = " etransfer " % } selected { % endif % } > e-Transfer< / option >
< option value = "square" { % if pay_mode = = " square " % } selected { % endif % } > Credit Card< / option >
< option value = "crypto" { % if pay_mode = = " crypto " % } selected { % endif % } > Crypto< / option >
< / select >
< / div >
{% if (invoice.status or "")|lower != "paid" %}
< div id = "panel-etransfer" class = "pay-panel{% if pay_mode != 'etransfer' %} hidden{% endif %}" >
< div class = "detail-card" style = "margin-top:1.25rem;" >
< p > < strong > Interac e-Transfer< / strong > < br >
< h3 > Payment Instructions< / h3 >
Send payment to:< br >
payment@outsidethebox.top< br >
< p > < strong > Interac e-Transfer< / strong > < br >
Reference: Invoice {{ invoice.invoice_number or ("INV-" ~ invoice.id) }}< / p >
Send payment to:< br >
< / div >
payment@outsidethebox.top< br >
Reference: Invoice {{ invoice.invoice_number or ("INV-" ~ invoice.id) }}
< / p >
< p > < strong > Credit Card (Square)< / strong > < br >
< a href = "/portal/invoice/{{ invoice.id }}/pay-square" target = "_blank" rel = "noopener noreferrer"
style="display:inline-block;padding:12px 18px;background:#16a34a;color:#ffffff;text-decoration:none;border-radius:8px;font-weight:700;margin:8px 0;">
Pay Now
< / a > < br >
Please include your invoice number in the payment note.
< / p >
< p >
If you have questions please contact
< a href = "mailto:support@outsidethebox.top?subject=Portal%20Support" > support@outsidethebox.top< / a >
< / p >
< / div >
{% endif %}
{% if invoice.oracle_quote and invoice.oracle_quote.quotes %}
< div id = "panel-square" class = "pay-panel{% if pay_mode != 'square' %} hidden{% endif %}" >
< div class = "detail-card" style = "margin-top:1.25rem;" >
< p > < strong > Credit Card (Square)< / strong > < / p >
< h3 > Crypto Quote Snapshot< / h3 >
< a href = "/portal/invoice/{{ invoice.id }}/pay-square" target = "_blank" rel = "noopener noreferrer" class = "pay-btn pay-btn-square" >
< div class = "quote-meta" >
Pay with Credit Card
< div > < strong > Quoted At:< / strong > {{ invoice.oracle_quote.quoted_at or "—" }}< / div >
< / a >
< div > < strong > Quote Expires:< / strong > {{ invoice.quote_expires_at_local or (invoice.oracle_quote.expires_at or "—") }}< / div >
< div > < strong > Source Status:< / strong > {{ invoice.oracle_quote.source_status or "—" }}< / div >
< div > < strong > Frozen Amount:< / strong > {{ invoice.oracle_quote.amount or invoice.quote_fiat_amount or invoice.total_amount }} {{ invoice.oracle_quote.fiat or invoice.quote_fiat_currency or "CAD" }}< / div >
< / div >
< / div >
< table class = "quote-table" >
< div id = "panel-crypto" class = "pay-panel{% if pay_mode != 'crypto' %} hidden{% endif %}" >
< thead >
{% if invoice.oracle_quote and invoice.oracle_quote.quotes and crypto_options %}
< tr >
< div class = "snapshot-wrap" >
< th > Asset< / th >
< div class = "snapshot-header" >
< th > Quoted Amount< / th >
< div class = "snapshot-meta" >
< th > CAD Price< / th >
< h3 style = "margin-top:0;" > Crypto Quote Snapshot< / h3 >
< th > Status< / th >
< div > < strong > Quoted At:< / strong > {{ invoice.oracle_quote.quoted_at or "—" }}< / div >
< / tr >
< div > < strong > Source Status:< / strong > {{ invoice.oracle_quote.source_status or "—" }}< / div >
< / thead >
< div > < strong > Frozen Amount:< / strong > {{ invoice.oracle_quote.amount or invoice.quote_fiat_amount or invoice.total_amount }} {{ invoice.oracle_quote.fiat or invoice.quote_fiat_currency or "CAD" }}< / div >
< tbody >
{% if pending_crypto_payment %}
{% for q in invoice.oracle_quote.quotes %}
< div style = "margin-top:0.75rem;" > < strong > Price locked for 2 minutes after acceptance.< / strong > < / div >
< tr >
{% else %}
< td >
< div style = "margin-top:0.75rem;" > < strong > Select a crypto asset to accept the quote.< / strong > < / div >
{{ q.symbol }} {% if q.chain %}({{ q.chain }}){% endif %}
{% endif %}
{% if q.recommended %}
< / div >
< span class = "quote-badge quote-live" > recommended< / span >
{% endif %}
{% if pending_crypto_payment %}
< / td >
< div class = "snapshot-timer-box" >
< td > {{ q.display_amount or "—" }}< / td >
< div id = "lockTimerValue" class = "snapshot-timer-value" data-expiry = "{{ pending_crypto_payment.lock_expires_at_iso }}" > --:--< / div >
< td > {% if q.price_cad is not none %}{{ "%.8f"|format(q.price_cad|float) }}{% else %}—{% endif %}< / td >
< div id = "lockTimerLabel" class = "snapshot-timer-label" > This price is locked for 2 minutes< / div >
< td >
< / div >
{% if q.available %}
< span class = "quote-badge quote-live" > live< / span >
{% else %}
{% else %}
< span class = "quote-badge quote-stale" > {{ q.reason or "unavailable" }}< / span >
< div class = "snapshot-timer-box" >
< div id = "quoteTimerValue" class = "snapshot-timer-value" data-expiry = "{{ crypto_quote_window_expires_iso }}" > --:--< / div >
< div id = "quoteTimerLabel" class = "snapshot-timer-label" > This price times out:< / div >
< / div >
{% endif %}
{% endif %}
< / td >
< / div >
< / tr >
{% endfor %}
{% if pending_crypto_payment and selected_crypto_option %}
< / tbody >
< div id = "lockBox" class = "lock-box{% if pending_crypto_payment.lock_expired %} expired{% endif %}" >
< / table >
< div class = "lock-grid" >
< p style = "margin-top:0.85rem; opacity:0.9;" >
< div >
These crypto values were frozen when the invoice was created and are retained for audit/reference.
< h3 style = "margin-top:0;" > {{ selected_crypto_option.label }} Payment Instructions< / h3 >
< / p >
< div > < strong > Send exactly:< / strong > {{ pending_crypto_payment.payment_amount }} {{ pending_crypto_payment.payment_currency }}< / div >
< div style = "margin-top:0.65rem;" > < strong > Destination wallet:< / strong > < / div >
< code class = "lock-code" > {{ pending_crypto_payment.wallet_address }}< / code >
< div style = "margin-top:0.65rem;" > < strong > Reference / Invoice:< / strong > < / div >
< code class = "lock-code" > {{ pending_crypto_payment.reference }}< / code >
< div style = "margin-top:0.65rem;" >
{% if pending_crypto_payment.lock_expired %}
price has expired - please refresh your quote to update
{% else %}
Your selected crypto quote has been accepted and placed into processing.
{% endif %}
< / div >
< / div >
< div class = "snapshot-timer-box" >
< div id = "lockTimerSideValue" class = "snapshot-timer-value" data-expiry = "{{ pending_crypto_payment.lock_expires_at_iso }}" > --:--< / div >
< div id = "lockTimerSideLabel" class = "snapshot-timer-label" > This price is locked for 2 minutes< / div >
< / div >
< / div >
< / div >
{% else %}
< form id = "cryptoPickForm" method = "post" action = "/portal/invoice/{{ invoice.id }}/pay-crypto" >
< table class = "quote-table" >
< thead >
< tr >
< th > Asset< / th >
< th > Quoted Amount< / th >
< th > CAD Price< / th >
< th > Status< / th >
< th > Action< / th >
< / tr >
< / thead >
< tbody >
{% for q in crypto_options %}
< tr >
< td >
{{ q.label }}
{% if q.recommended %}
< span class = "quote-badge quote-live" > recommended< / span >
{% endif %}
< / td >
< td > {{ q.display_amount or "—" }}< / td >
< td > {% if q.price_cad is not none %}{{ "%.8f"|format(q.price_cad|float) }}{% else %}—{% endif %}< / td >
< td >
{% if q.available %}
< span class = "quote-badge quote-live" > live< / span >
{% else %}
< span class = "quote-badge quote-stale" > {{ q.reason or "unavailable" }}< / span >
{% endif %}
< / td >
< td >
< button type = "submit" name = "asset" value = "{{ q.symbol }}" class = "quote-pick-btn" { % if not q . available % } disabled { % endif % } >
Accept {{ q.symbol }}
< / button >
< / td >
< / tr >
{% endfor %}
< / tbody >
< / table >
< / form >
{% endif %}
< / div >
{% else %}
< p > No crypto quote snapshot is available for this invoice yet.< / p >
{% endif %}
< / div >
< / div >
< / div >
{% endif %}
{% endif %}
{% if pdf_url %}
{% if pdf_url %}
< div class = "invoice-actions" >
< div style = "margin-top:1rem; " >
< a href = "/portal/invoice/{{ invoice.id }}/pdf" target = "_blank" rel = "noopener noreferrer" > Open Invoice PDF< / a >
< a href = "/portal/invoice/{{ invoice.id }}/pdf" target = "_blank" rel = "noopener noreferrer" > Open Invoice PDF< / a >
< / div >
< / div >
{% endif %}
{% endif %}
< / div >
< / div >
< script >
(function() {
const select = document.getElementById("payMethodSelect");
if (select) {
select.addEventListener("change", function() {
const value = this.value || "";
const url = new URL(window.location.href);
if (!value) {
url.searchParams.delete("pay");
url.searchParams.delete("asset");
url.searchParams.delete("payment_id");
url.searchParams.delete("crypto_error");
url.searchParams.delete("refresh_quote");
} else {
url.searchParams.set("pay", value);
if (value !== "crypto") {
url.searchParams.delete("asset");
url.searchParams.delete("payment_id");
url.searchParams.delete("crypto_error");
url.searchParams.delete("refresh_quote");
} else {
url.searchParams.delete("asset");
url.searchParams.delete("payment_id");
url.searchParams.delete("crypto_error");
url.searchParams.set("refresh_quote", "1");
}
}
window.location.href = url.toString();
});
}
function bindCountdown(valueId, labelId, expireIso, expiredMessage) {
const valueEl = document.getElementById(valueId);
const labelEl = document.getElementById(labelId);
if (!valueEl || !expireIso) return;
function tick() {
const end = new Date(expireIso).getTime();
const now = Date.now();
const diff = Math.max(0, Math.floor((end - now) / 1000));
if (diff < = 0) {
valueEl.textContent = "00:00";
valueEl.classList.add("snapshot-timer-expired");
if (labelEl) {
labelEl.textContent = expiredMessage;
labelEl.classList.add("snapshot-timer-expired");
}
const cryptoForm = document.getElementById("cryptoPickForm");
if (cryptoForm) {
cryptoForm.querySelectorAll("button").forEach(btn => btn.disabled = true);
}
const lockBox = document.getElementById("lockBox");
if (lockBox) {
lockBox.classList.add("expired");
}
return;
}
const m = String(Math.floor(diff / 60)).padStart(2, "0");
const s = String(diff % 60).padStart(2, "0");
valueEl.textContent = `${m}:${s}`;
setTimeout(tick, 250);
}
tick();
}
const quoteTimer = document.getElementById("quoteTimerValue");
if (quoteTimer & & quoteTimer.dataset.expiry) {
bindCountdown(
"quoteTimerValue",
"quoteTimerLabel",
quoteTimer.dataset.expiry,
"price has expired - please refresh your view to update"
);
}
const lockTimer = document.getElementById("lockTimerValue");
if (lockTimer & & lockTimer.dataset.expiry) {
bindCountdown(
"lockTimerValue",
"lockTimerLabel",
lockTimer.dataset.expiry,
"price has expired - please refresh your quote to update"
);
}
const lockTimerSide = document.getElementById("lockTimerSideValue");
if (lockTimerSide & & lockTimerSide.dataset.expiry) {
bindCountdown(
"lockTimerSideValue",
"lockTimerSideLabel",
lockTimerSide.dataset.expiry,
"price has expired - please refresh your quote to update"
);
}
})();
< / script >
{% include "footer.html" %}
{% include "footer.html" %}
< / body >
< / body >
< / html >
< / html >