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
59 changes: 55 additions & 4 deletions addon/components/customer-invoice.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,13 @@
</div>
{{/if}}

{{#if this.pendingMessage}}
<div class="rounded-xl bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-700 p-6 flex items-start gap-4 mb-6">
<FaIcon @icon="clock" class="text-blue-500 mt-0.5 shrink-0" />
<p class="text-blue-700 dark:text-blue-300 font-medium">{{this.pendingMessage}}</p>
</div>
{{/if}}

{{! ── Payment section ──────────────────────────────────────────── }}
{{#if this.canAcceptPayment}}
<div class="bg-white dark:bg-gray-800 rounded-2xl shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden">
Expand Down Expand Up @@ -180,11 +187,55 @@
/>
</div>

{{! Redirecting to Stripe overlay }}
{{! Redirecting to payment provider overlay }}
{{#if this.isRedirectingToCheckout}}
<div class="flex flex-col items-center justify-center py-6 gap-3 text-indigo-600 dark:text-indigo-400">
<FaIcon @icon="spinner" @spin={{true}} @size="lg" />
<p class="text-sm font-medium">Redirecting to secure payment page…</p>
<p class="text-sm font-medium">Redirecting to payment provider...</p>
</div>
{{else if this.hasTalerPaymentUri}}
<div class="rounded-xl border border-indigo-200 dark:border-indigo-700 bg-indigo-50 dark:bg-indigo-900/20 p-4 flex flex-col gap-3">
<div class="flex flex-col sm:flex-row items-start gap-4">
{{#if this.paymentQrImageSrc}}
<div class="shrink-0 w-40 h-40 rounded-lg border border-white/70 dark:border-indigo-700 bg-white p-2 flex items-center justify-center">
<img src={{this.paymentQrImageSrc}} alt="GNU Taler payment QR code" class="w-full h-full object-contain" />
</div>
{{/if}}
<div class="min-w-0 flex-1">
<div class="flex items-start gap-3">
<FaIcon @icon="wallet" class="text-indigo-500 mt-0.5" />
<div>
<p class="text-sm font-semibold text-indigo-800 dark:text-indigo-200">GNU Taler payment ready</p>
<p class="text-sm text-indigo-700 dark:text-indigo-300 mt-1">
{{#if this.paymentQrImageSrc}}
Open your Taler wallet or scan the QR code with a wallet on another device.
{{else}}
Open your Taler wallet from this browser, or copy the payment URI into a wallet.
{{/if}}
</p>
</div>
</div>
{{#unless this.paymentQrImageSrc}}
<div class="mt-3 rounded-md border border-amber-200 dark:border-amber-700 bg-amber-50 dark:bg-amber-900/20 px-3 py-2 text-xs text-amber-700 dark:text-amber-300">
QR code unavailable for this payment response.
</div>
{{/unless}}
{{#if this.paymentQrText}}
<div class="mt-3 rounded-md bg-white/70 dark:bg-gray-900/30 border border-indigo-100 dark:border-indigo-800 px-3 py-2">
<p class="font-mono text-[11px] leading-4 text-indigo-700 dark:text-indigo-300 break-all">{{this.paymentQrText}}</p>
</div>
{{/if}}
</div>
</div>
<div class="flex items-center justify-end gap-3">
<a
href={{this.talerPaymentUri}}
class="inline-flex items-center justify-center rounded-md bg-indigo-600 px-4 py-2 text-sm font-semibold text-white hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 dark:focus:ring-offset-gray-800"
>
<FaIcon @icon="wallet" class="mr-2" />
Open GNU Taler Wallet
</a>
</div>
</div>
{{else}}
{{! Submit }}
Expand All @@ -199,8 +250,8 @@
@type="primary"
@icon={{if this.submitPayment.isRunning "spinner" "lock"}}
@iconSpin={{this.submitPayment.isRunning}}
@text={{if this.submitPayment.isRunning "Processing" (if this.isStripeGateway "Pay with Stripe" "Confirm Payment")}}
@disabled={{or this.submitPayment.isRunning (not this.selectedGatewayId)}}
@text={{if this.submitPayment.isRunning "Processing..." (if this.isStripeGateway "Pay with Stripe" "Continue to Payment")}}
@disabled={{this.submitPayment.isRunning}}
@onClick={{perform this.submitPayment}}
/>
</div>
Expand Down
89 changes: 83 additions & 6 deletions addon/components/customer-invoice.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,23 @@ export default class CustomerInvoiceComponent extends Component {
@tracked paymentReference = '';
@tracked error = null;
@tracked successMessage = null;
@tracked pendingMessage = null;
@tracked isRedirectingToCheckout = false;
@tracked talerPaymentUri = null;
@tracked paymentQrImage = null;
@tracked paymentQrText = null;

constructor() {
super(...arguments);
this.installTalerSupportMeta();
this.loadInvoice.perform();
}

willDestroy() {
super.willDestroy(...arguments);
this.removeTalerSupportMeta();
}

// ── Getters ───────────────────────────────────────────────────────────────

get invoiceId() {
Expand Down Expand Up @@ -72,6 +82,22 @@ export default class CustomerInvoiceComponent extends Component {
return this.selectedGateway?.driver === 'stripe';
}

get hasTalerPaymentUri() {
return typeof this.talerPaymentUri === 'string' && this.talerPaymentUri.startsWith('taler');
}

get paymentQrImageSrc() {
if (typeof this.paymentQrImage !== 'string' || this.paymentQrImage.length === 0) {
return null;
}

if (this.paymentQrImage.startsWith('data:') || this.paymentQrImage.startsWith('http://') || this.paymentQrImage.startsWith('https://')) {
return this.paymentQrImage;
}

return `data:image/png;base64,${this.paymentQrImage}`;
}

// ── Tasks ─────────────────────────────────────────────────────────────────

/**
Expand Down Expand Up @@ -133,9 +159,8 @@ export default class CustomerInvoiceComponent extends Component {
/**
* Submits a payment request.
*
* For Stripe: backend returns { checkout_url } and the browser is redirected
* to Stripe's hosted checkout page. isRedirectingToCheckout is set to true
* to show a loading state while the redirect happens.
* For redirect-capable gateways: backend returns { payment_url } or
* { checkout_url } and the browser is redirected to the hosted/wallet flow.
*
* For other gateways: backend records the payment immediately and returns
* the updated invoice.
Expand All @@ -159,10 +184,28 @@ export default class CustomerInvoiceComponent extends Component {
{ namespace: 'ledger/public' }
);

// Stripe Checkout Session — redirect the browser to Stripe's hosted page
if (data?.checkout_url) {
const paymentUrl = data?.payment_url ?? data?.payment_uri ?? data?.checkout_url ?? data?.data?.taler_pay_uri;
this.paymentQrImage = data?.qr_image ?? data?.data?.qr_image ?? null;
this.paymentQrText = data?.qr_text ?? data?.data?.qr_text ?? paymentUrl ?? null;

if (this.isTalerUri(paymentUrl)) {
this.talerPaymentUri = paymentUrl;
this.isRedirectingToCheckout = false;
this.pendingMessage = data?.message ?? 'Payment started. Open your GNU Taler wallet to complete it.';
return;
}

// Redirect payment sessions — Stripe Checkout, QPay app link, etc.
if (paymentUrl) {
this.isRedirectingToCheckout = true;
window.location.href = data.checkout_url;
this.pendingMessage = data?.message ?? 'Redirecting to payment provider...';
window.location.href = paymentUrl;
return;
}

if (data?.payment_status === 'pending') {
this.pendingMessage = data?.message ?? 'Payment started. Complete it in your payment app, then refresh this invoice.';
this.showPaymentForm = false;
return;
}

Expand All @@ -180,6 +223,10 @@ export default class CustomerInvoiceComponent extends Component {
@action togglePaymentForm() {
this.showPaymentForm = !this.showPaymentForm;
this.successMessage = null;
this.pendingMessage = null;
this.talerPaymentUri = null;
this.paymentQrImage = null;
this.paymentQrText = null;
this.error = null;
}

Expand All @@ -190,4 +237,34 @@ export default class CustomerInvoiceComponent extends Component {
@action updateReference(event) {
this.paymentReference = event.target.value;
}

isTalerUri(value) {
return typeof value === 'string' && (value.startsWith('taler://') || value.startsWith('taler+http://') || value.startsWith('taler+https://'));
}

installTalerSupportMeta() {
if (typeof document === 'undefined') {
return;
}

let meta = document.querySelector('meta[name="taler-support"]');

if (!meta) {
meta = document.createElement('meta');
meta.name = 'taler-support';
meta.dataset.ledgerTalerSupport = 'true';
document.head.appendChild(meta);
}

meta.content = 'uri';
}

removeTalerSupportMeta() {
if (typeof document === 'undefined') {
return;
}

const meta = document.querySelector('meta[name="taler-support"][data-ledger-taler-support="true"]');
meta?.remove();
}
}
43 changes: 41 additions & 2 deletions addon/controllers/billing/invoices/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,18 @@ export default class BillingInvoicesIndexController extends Controller {
@service hostRouter;
@service intl;

@tracked queryParams = ['page', 'limit', 'sort', 'query', 'status', 'customer_uuid'];
@tracked queryParams = ['page', 'limit', 'sort', 'query', 'status', 'currency', 'order', 'customer', 'created_at', 'due_date', 'amount'];
@tracked page = 1;
@tracked limit = 30;
@tracked sort = '-created_at';
@tracked query = null;
@tracked status = null;
@tracked customer_uuid = null;
@tracked currency = null;
@tracked order = null;
@tracked customer = null;
@tracked created_at = null;
@tracked due_date = null;
@tracked amount = null;
@tracked table = null;

get actionButtons() {
Expand Down Expand Up @@ -64,6 +69,11 @@ export default class BillingInvoicesIndexController extends Controller {
valuePath: 'customerName',
resizable: true,
sortable: false,
filterable: true,
filterParam: 'customer',
filterComponent: 'filter/model',
filterComponentPlaceholder: 'Select customer',
model: 'customer',
},
{
label: 'Order',
Expand All @@ -72,6 +82,12 @@ export default class BillingInvoicesIndexController extends Controller {
action: this.viewOrder,
resizable: true,
sortable: false,
filterable: true,
filterParam: 'order',
filterComponent: 'filter/model',
filterComponentPlaceholder: 'Select order',
model: 'order',
modelNamePath: 'tracking',
},
{
label: this.intl.t('column.status'),
Expand Down Expand Up @@ -100,6 +116,23 @@ export default class BillingInvoicesIndexController extends Controller {
currencyPath: 'currency',
resizable: true,
sortable: true,
filterable: true,
filterParam: 'amount',
filterComponent: 'filter/range',
min: 0,
max: 10000000,
step: 100,
minLabel: 'Min',
maxLabel: 'Max',
},
{
label: this.intl.t('column.currency'),
valuePath: 'currency',
resizable: true,
sortable: true,
filterable: true,
filterParam: 'currency',
filterComponent: 'filter/string',
},
{
label: this.intl.t('column.balance'),
Expand All @@ -114,6 +147,9 @@ export default class BillingInvoicesIndexController extends Controller {
valuePath: 'dueDate',
resizable: true,
sortable: true,
filterable: true,
filterParam: 'due_date',
filterComponent: 'filter/date',
},
{
label: this.intl.t('column.invoice-date'),
Expand All @@ -126,6 +162,9 @@ export default class BillingInvoicesIndexController extends Controller {
valuePath: 'createdAt',
resizable: true,
sortable: true,
filterable: true,
filterParam: 'created_at',
filterComponent: 'filter/date',
},
{
label: '',
Expand Down
16 changes: 15 additions & 1 deletion addon/controllers/payments/transactions/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export default class PaymentsTransactionsIndexController extends Controller {
'description',
'type',
'status',
'settlement_status',
'direction',
'currency',
'gateway',
Expand All @@ -35,6 +36,7 @@ export default class PaymentsTransactionsIndexController extends Controller {
@tracked description = null;
@tracked type = null;
@tracked status = null;
@tracked settlement_status = null;
@tracked direction = null;
@tracked currency = null;
@tracked gateway = null;
Expand Down Expand Up @@ -141,7 +143,19 @@ export default class PaymentsTransactionsIndexController extends Controller {
filterable: true,
filterParam: 'status',
filterComponent: 'filter/multi-option',
filterOptions: ['pending', 'succeeded', 'failed', 'refunded', 'voided', 'reversed', 'expired'],
filterOptions: ['pending', 'success', 'failed', 'cancelled', 'voided', 'reversed', 'expired'],
},
{
label: 'Settlement',
valuePath: 'settlement_status',
cellComponent: 'table/cell/status',
hidden: true,
resizable: true,
sortable: true,
filterable: true,
filterParam: 'settlement_status',
filterComponent: 'filter/multi-option',
filterOptions: ['unpaid', 'partially_paid', 'paid', 'partially_refunded', 'refunded'],
},
// ── Hidden / toggleable columns ──────────────────────────────────
{
Expand Down
1 change: 1 addition & 0 deletions addon/models/ledger-transaction.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export default class LedgerTransactionModel extends Model {
@attr('string') type;
@attr('string') direction;
@attr('string') status;
@attr('string') settlement_status;

@computed('direction') get direction_sign() {
return this.direction === 'debit' ? '-' : '+';
Expand Down
6 changes: 6 additions & 0 deletions addon/routes/billing/invoices/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@ export default class BillingInvoicesIndexRoute extends Route {
sort: { refreshModel: true },
query: { refreshModel: true },
status: { refreshModel: true },
currency: { refreshModel: true },
order: { refreshModel: true },
customer: { refreshModel: true },
created_at: { refreshModel: true },
due_date: { refreshModel: true },
amount: { refreshModel: true },
};

model(params) {
Expand Down
1 change: 1 addition & 0 deletions addon/routes/payments/transactions/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export default class PaymentsTransactionsIndexRoute extends Route {
sort: { refreshModel: true },
query: { refreshModel: true },
status: { refreshModel: true },
settlement_status: { refreshModel: true },
};

model(params) {
Expand Down
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "fleetbase/ledger-api",
"version": "0.0.5",
"version": "0.0.6",
"description": "Accounting & Invoicing Extension for Fleetbase",
"keywords": [
"fleetbase",
Expand Down
2 changes: 2 additions & 0 deletions docker/taler/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
generated-token.txt
*.local
Loading
Loading