Skip to content

Commit 3d9b769

Browse files
authored
Merge pull request #11 from fleetbase/dev-v0.0.6
Add revenue lifecycle safeguards
2 parents 964f4b5 + fdf3501 commit 3d9b769

43 files changed

Lines changed: 4206 additions & 568 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

addon/components/customer-invoice.hbs

Lines changed: 55 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,13 @@
120120
</div>
121121
{{/if}}
122122

123+
{{#if this.pendingMessage}}
124+
<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">
125+
<FaIcon @icon="clock" class="text-blue-500 mt-0.5 shrink-0" />
126+
<p class="text-blue-700 dark:text-blue-300 font-medium">{{this.pendingMessage}}</p>
127+
</div>
128+
{{/if}}
129+
123130
{{! ── Payment section ──────────────────────────────────────────── }}
124131
{{#if this.canAcceptPayment}}
125132
<div class="bg-white dark:bg-gray-800 rounded-2xl shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden">
@@ -180,11 +187,55 @@
180187
/>
181188
</div>
182189

183-
{{! Redirecting to Stripe overlay }}
190+
{{! Redirecting to payment provider overlay }}
184191
{{#if this.isRedirectingToCheckout}}
185192
<div class="flex flex-col items-center justify-center py-6 gap-3 text-indigo-600 dark:text-indigo-400">
186193
<FaIcon @icon="spinner" @spin={{true}} @size="lg" />
187-
<p class="text-sm font-medium">Redirecting to secure payment page…</p>
194+
<p class="text-sm font-medium">Redirecting to payment provider...</p>
195+
</div>
196+
{{else if this.hasTalerPaymentUri}}
197+
<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">
198+
<div class="flex flex-col sm:flex-row items-start gap-4">
199+
{{#if this.paymentQrImageSrc}}
200+
<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">
201+
<img src={{this.paymentQrImageSrc}} alt="GNU Taler payment QR code" class="w-full h-full object-contain" />
202+
</div>
203+
{{/if}}
204+
<div class="min-w-0 flex-1">
205+
<div class="flex items-start gap-3">
206+
<FaIcon @icon="wallet" class="text-indigo-500 mt-0.5" />
207+
<div>
208+
<p class="text-sm font-semibold text-indigo-800 dark:text-indigo-200">GNU Taler payment ready</p>
209+
<p class="text-sm text-indigo-700 dark:text-indigo-300 mt-1">
210+
{{#if this.paymentQrImageSrc}}
211+
Open your Taler wallet or scan the QR code with a wallet on another device.
212+
{{else}}
213+
Open your Taler wallet from this browser, or copy the payment URI into a wallet.
214+
{{/if}}
215+
</p>
216+
</div>
217+
</div>
218+
{{#unless this.paymentQrImageSrc}}
219+
<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">
220+
QR code unavailable for this payment response.
221+
</div>
222+
{{/unless}}
223+
{{#if this.paymentQrText}}
224+
<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">
225+
<p class="font-mono text-[11px] leading-4 text-indigo-700 dark:text-indigo-300 break-all">{{this.paymentQrText}}</p>
226+
</div>
227+
{{/if}}
228+
</div>
229+
</div>
230+
<div class="flex items-center justify-end gap-3">
231+
<a
232+
href={{this.talerPaymentUri}}
233+
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"
234+
>
235+
<FaIcon @icon="wallet" class="mr-2" />
236+
Open GNU Taler Wallet
237+
</a>
238+
</div>
188239
</div>
189240
{{else}}
190241
{{! Submit }}
@@ -199,8 +250,8 @@
199250
@type="primary"
200251
@icon={{if this.submitPayment.isRunning "spinner" "lock"}}
201252
@iconSpin={{this.submitPayment.isRunning}}
202-
@text={{if this.submitPayment.isRunning "Processing" (if this.isStripeGateway "Pay with Stripe" "Confirm Payment")}}
203-
@disabled={{or this.submitPayment.isRunning (not this.selectedGatewayId)}}
253+
@text={{if this.submitPayment.isRunning "Processing..." (if this.isStripeGateway "Pay with Stripe" "Continue to Payment")}}
254+
@disabled={{this.submitPayment.isRunning}}
204255
@onClick={{perform this.submitPayment}}
205256
/>
206257
</div>

addon/components/customer-invoice.js

Lines changed: 83 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,13 +31,23 @@ export default class CustomerInvoiceComponent extends Component {
3131
@tracked paymentReference = '';
3232
@tracked error = null;
3333
@tracked successMessage = null;
34+
@tracked pendingMessage = null;
3435
@tracked isRedirectingToCheckout = false;
36+
@tracked talerPaymentUri = null;
37+
@tracked paymentQrImage = null;
38+
@tracked paymentQrText = null;
3539

3640
constructor() {
3741
super(...arguments);
42+
this.installTalerSupportMeta();
3843
this.loadInvoice.perform();
3944
}
4045

46+
willDestroy() {
47+
super.willDestroy(...arguments);
48+
this.removeTalerSupportMeta();
49+
}
50+
4151
// ── Getters ───────────────────────────────────────────────────────────────
4252

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

85+
get hasTalerPaymentUri() {
86+
return typeof this.talerPaymentUri === 'string' && this.talerPaymentUri.startsWith('taler');
87+
}
88+
89+
get paymentQrImageSrc() {
90+
if (typeof this.paymentQrImage !== 'string' || this.paymentQrImage.length === 0) {
91+
return null;
92+
}
93+
94+
if (this.paymentQrImage.startsWith('data:') || this.paymentQrImage.startsWith('http://') || this.paymentQrImage.startsWith('https://')) {
95+
return this.paymentQrImage;
96+
}
97+
98+
return `data:image/png;base64,${this.paymentQrImage}`;
99+
}
100+
75101
// ── Tasks ─────────────────────────────────────────────────────────────────
76102

77103
/**
@@ -133,9 +159,8 @@ export default class CustomerInvoiceComponent extends Component {
133159
/**
134160
* Submits a payment request.
135161
*
136-
* For Stripe: backend returns { checkout_url } and the browser is redirected
137-
* to Stripe's hosted checkout page. isRedirectingToCheckout is set to true
138-
* to show a loading state while the redirect happens.
162+
* For redirect-capable gateways: backend returns { payment_url } or
163+
* { checkout_url } and the browser is redirected to the hosted/wallet flow.
139164
*
140165
* For other gateways: backend records the payment immediately and returns
141166
* the updated invoice.
@@ -159,10 +184,28 @@ export default class CustomerInvoiceComponent extends Component {
159184
{ namespace: 'ledger/public' }
160185
);
161186

162-
// Stripe Checkout Session — redirect the browser to Stripe's hosted page
163-
if (data?.checkout_url) {
187+
const paymentUrl = data?.payment_url ?? data?.payment_uri ?? data?.checkout_url ?? data?.data?.taler_pay_uri;
188+
this.paymentQrImage = data?.qr_image ?? data?.data?.qr_image ?? null;
189+
this.paymentQrText = data?.qr_text ?? data?.data?.qr_text ?? paymentUrl ?? null;
190+
191+
if (this.isTalerUri(paymentUrl)) {
192+
this.talerPaymentUri = paymentUrl;
193+
this.isRedirectingToCheckout = false;
194+
this.pendingMessage = data?.message ?? 'Payment started. Open your GNU Taler wallet to complete it.';
195+
return;
196+
}
197+
198+
// Redirect payment sessions — Stripe Checkout, QPay app link, etc.
199+
if (paymentUrl) {
164200
this.isRedirectingToCheckout = true;
165-
window.location.href = data.checkout_url;
201+
this.pendingMessage = data?.message ?? 'Redirecting to payment provider...';
202+
window.location.href = paymentUrl;
203+
return;
204+
}
205+
206+
if (data?.payment_status === 'pending') {
207+
this.pendingMessage = data?.message ?? 'Payment started. Complete it in your payment app, then refresh this invoice.';
208+
this.showPaymentForm = false;
166209
return;
167210
}
168211

@@ -180,6 +223,10 @@ export default class CustomerInvoiceComponent extends Component {
180223
@action togglePaymentForm() {
181224
this.showPaymentForm = !this.showPaymentForm;
182225
this.successMessage = null;
226+
this.pendingMessage = null;
227+
this.talerPaymentUri = null;
228+
this.paymentQrImage = null;
229+
this.paymentQrText = null;
183230
this.error = null;
184231
}
185232

@@ -190,4 +237,34 @@ export default class CustomerInvoiceComponent extends Component {
190237
@action updateReference(event) {
191238
this.paymentReference = event.target.value;
192239
}
240+
241+
isTalerUri(value) {
242+
return typeof value === 'string' && (value.startsWith('taler://') || value.startsWith('taler+http://') || value.startsWith('taler+https://'));
243+
}
244+
245+
installTalerSupportMeta() {
246+
if (typeof document === 'undefined') {
247+
return;
248+
}
249+
250+
let meta = document.querySelector('meta[name="taler-support"]');
251+
252+
if (!meta) {
253+
meta = document.createElement('meta');
254+
meta.name = 'taler-support';
255+
meta.dataset.ledgerTalerSupport = 'true';
256+
document.head.appendChild(meta);
257+
}
258+
259+
meta.content = 'uri';
260+
}
261+
262+
removeTalerSupportMeta() {
263+
if (typeof document === 'undefined') {
264+
return;
265+
}
266+
267+
const meta = document.querySelector('meta[name="taler-support"][data-ledger-taler-support="true"]');
268+
meta?.remove();
269+
}
193270
}

addon/controllers/billing/invoices/index.js

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,18 @@ export default class BillingInvoicesIndexController extends Controller {
99
@service hostRouter;
1010
@service intl;
1111

12-
@tracked queryParams = ['page', 'limit', 'sort', 'query', 'status', 'customer_uuid'];
12+
@tracked queryParams = ['page', 'limit', 'sort', 'query', 'status', 'currency', 'order', 'customer', 'created_at', 'due_date', 'amount'];
1313
@tracked page = 1;
1414
@tracked limit = 30;
1515
@tracked sort = '-created_at';
1616
@tracked query = null;
1717
@tracked status = null;
18-
@tracked customer_uuid = null;
18+
@tracked currency = null;
19+
@tracked order = null;
20+
@tracked customer = null;
21+
@tracked created_at = null;
22+
@tracked due_date = null;
23+
@tracked amount = null;
1924
@tracked table = null;
2025

2126
get actionButtons() {
@@ -64,6 +69,11 @@ export default class BillingInvoicesIndexController extends Controller {
6469
valuePath: 'customerName',
6570
resizable: true,
6671
sortable: false,
72+
filterable: true,
73+
filterParam: 'customer',
74+
filterComponent: 'filter/model',
75+
filterComponentPlaceholder: 'Select customer',
76+
model: 'customer',
6777
},
6878
{
6979
label: 'Order',
@@ -72,6 +82,12 @@ export default class BillingInvoicesIndexController extends Controller {
7282
action: this.viewOrder,
7383
resizable: true,
7484
sortable: false,
85+
filterable: true,
86+
filterParam: 'order',
87+
filterComponent: 'filter/model',
88+
filterComponentPlaceholder: 'Select order',
89+
model: 'order',
90+
modelNamePath: 'tracking',
7591
},
7692
{
7793
label: this.intl.t('column.status'),
@@ -100,6 +116,23 @@ export default class BillingInvoicesIndexController extends Controller {
100116
currencyPath: 'currency',
101117
resizable: true,
102118
sortable: true,
119+
filterable: true,
120+
filterParam: 'amount',
121+
filterComponent: 'filter/range',
122+
min: 0,
123+
max: 10000000,
124+
step: 100,
125+
minLabel: 'Min',
126+
maxLabel: 'Max',
127+
},
128+
{
129+
label: this.intl.t('column.currency'),
130+
valuePath: 'currency',
131+
resizable: true,
132+
sortable: true,
133+
filterable: true,
134+
filterParam: 'currency',
135+
filterComponent: 'filter/string',
103136
},
104137
{
105138
label: this.intl.t('column.balance'),
@@ -114,6 +147,9 @@ export default class BillingInvoicesIndexController extends Controller {
114147
valuePath: 'dueDate',
115148
resizable: true,
116149
sortable: true,
150+
filterable: true,
151+
filterParam: 'due_date',
152+
filterComponent: 'filter/date',
117153
},
118154
{
119155
label: this.intl.t('column.invoice-date'),
@@ -126,6 +162,9 @@ export default class BillingInvoicesIndexController extends Controller {
126162
valuePath: 'createdAt',
127163
resizable: true,
128164
sortable: true,
165+
filterable: true,
166+
filterParam: 'created_at',
167+
filterComponent: 'filter/date',
129168
},
130169
{
131170
label: '',

addon/controllers/payments/transactions/index.js

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export default class PaymentsTransactionsIndexController extends Controller {
1616
'description',
1717
'type',
1818
'status',
19+
'settlement_status',
1920
'direction',
2021
'currency',
2122
'gateway',
@@ -35,6 +36,7 @@ export default class PaymentsTransactionsIndexController extends Controller {
3536
@tracked description = null;
3637
@tracked type = null;
3738
@tracked status = null;
39+
@tracked settlement_status = null;
3840
@tracked direction = null;
3941
@tracked currency = null;
4042
@tracked gateway = null;
@@ -141,7 +143,19 @@ export default class PaymentsTransactionsIndexController extends Controller {
141143
filterable: true,
142144
filterParam: 'status',
143145
filterComponent: 'filter/multi-option',
144-
filterOptions: ['pending', 'succeeded', 'failed', 'refunded', 'voided', 'reversed', 'expired'],
146+
filterOptions: ['pending', 'success', 'failed', 'cancelled', 'voided', 'reversed', 'expired'],
147+
},
148+
{
149+
label: 'Settlement',
150+
valuePath: 'settlement_status',
151+
cellComponent: 'table/cell/status',
152+
hidden: true,
153+
resizable: true,
154+
sortable: true,
155+
filterable: true,
156+
filterParam: 'settlement_status',
157+
filterComponent: 'filter/multi-option',
158+
filterOptions: ['unpaid', 'partially_paid', 'paid', 'partially_refunded', 'refunded'],
145159
},
146160
// ── Hidden / toggleable columns ──────────────────────────────────
147161
{

addon/models/ledger-transaction.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export default class LedgerTransactionModel extends Model {
1010
@attr('string') type;
1111
@attr('string') direction;
1212
@attr('string') status;
13+
@attr('string') settlement_status;
1314

1415
@computed('direction') get direction_sign() {
1516
return this.direction === 'debit' ? '-' : '+';

addon/routes/billing/invoices/index.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,12 @@ export default class BillingInvoicesIndexRoute extends Route {
1010
sort: { refreshModel: true },
1111
query: { refreshModel: true },
1212
status: { refreshModel: true },
13+
currency: { refreshModel: true },
14+
order: { refreshModel: true },
15+
customer: { refreshModel: true },
16+
created_at: { refreshModel: true },
17+
due_date: { refreshModel: true },
18+
amount: { refreshModel: true },
1319
};
1420

1521
model(params) {

addon/routes/payments/transactions/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export default class PaymentsTransactionsIndexRoute extends Route {
1010
sort: { refreshModel: true },
1111
query: { refreshModel: true },
1212
status: { refreshModel: true },
13+
settlement_status: { refreshModel: true },
1314
};
1415

1516
model(params) {

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "fleetbase/ledger-api",
3-
"version": "0.0.5",
3+
"version": "0.0.6",
44
"description": "Accounting & Invoicing Extension for Fleetbase",
55
"keywords": [
66
"fleetbase",

docker/taler/.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
generated-token.txt
2+
*.local

0 commit comments

Comments
 (0)