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
25 changes: 25 additions & 0 deletions .github/workflows/postman.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
name: API Contract (Postman)

# Boots a full Fleetbase stack (published image) and runs the Ledger API Postman
# collection against the live API. Delegates to the reusable workflow in
# fleetbase/fleetbase. Requires org secrets POSTMAN_API_KEY + _GITHUB_AUTH_TOKEN
# (inherited); no-ops until POSTMAN_API_KEY is set.
# TODO: change @dev-v0.7.53 to @main once that branch is merged.

on:
push:
branches: [main]
pull_request:
branches: [main]
workflow_dispatch:

permissions:
contents: read

jobs:
contract:
uses: fleetbase/fleetbase/.github/workflows/api-contract.yml@dev-v0.7.53
with:
collections: "Fleetbase Ledger API"
build-from-source: false
secrets: inherit
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
9 changes: 9 additions & 0 deletions .github/workflows/server.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,12 @@ jobs:
name: ledger-coverage-clover
path: coverage/clover.xml
if-no-files-found: error

- name: Upload coverage to Codecov
uses: codecov/codecov-action@v5
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: coverage/clover.xml
disable_search: true
flags: backend
fail_ci_if_error: false
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
<a href="https://github.com/fleetbase/ledger/blob/main/LICENSE.md"><img src="https://img.shields.io/badge/license-AGPL--3.0--or--later-blue.svg" alt="License: AGPL-3.0-or-later" /></a>
<a href="https://github.com/fleetbase/ledger/actions/workflows/server.yml"><img src="https://github.com/fleetbase/ledger/actions/workflows/server.yml/badge.svg" alt="PHP CI" /></a>
<a href="https://github.com/fleetbase/ledger/actions/workflows/ember.yml"><img src="https://github.com/fleetbase/ledger/actions/workflows/ember.yml/badge.svg" alt="Ember CI" /></a>
<a href="https://codecov.io/gh/fleetbase/ledger"><img src="https://codecov.io/gh/fleetbase/ledger/branch/main/graph/badge.svg" alt="Coverage" /></a>
<a href="https://packagist.org/packages/fleetbase/ledger-api"><img src="https://img.shields.io/packagist/v/fleetbase/ledger-api.svg" alt="Packagist version" /></a>
<a href="https://www.npmjs.com/package/@fleetbase/ledger-engine"><img src="https://img.shields.io/npm/v/@fleetbase/ledger-engine.svg" alt="npm version" /></a>
<a href="https://www.fleetbase.io/docs/ledger"><img src="https://img.shields.io/badge/docs-ledger-111827.svg" alt="Ledger documentation" /></a>
Expand Down
2 changes: 1 addition & 1 deletion addon/components/customer-invoice.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ export default class CustomerInvoiceComponent extends Component {
}

get isPaid() {
return this.invoice?.status === 'paid';
return ['paid', 'refunded', 'refund_pending', 'partial_refund_pending'].includes(this.invoice?.status);
}

get isVoid() {
Expand Down
61 changes: 61 additions & 0 deletions addon/components/customer-taler-refund.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
<div class="customer-invoice-page min-h-screen bg-gray-50 px-4 py-10 dark:bg-gray-900">
<div class="mx-auto max-w-2xl pt-10">
{{#if this.loadRefund.isRunning}}
<div class="flex flex-col items-center justify-center py-24 text-gray-500 dark:text-gray-400">
<FaIcon @icon="spinner" @spin={{true}} @size="2x" class="mb-4" />
<p class="text-sm">Loading refund...</p>
</div>
{{else if this.error}}
<div class="rounded-xl border border-red-200 bg-red-50 p-8 text-center dark:border-red-700 dark:bg-red-900/20">
<FaIcon @icon="triangle-exclamation" @size="2x" class="mb-4 text-red-400" />
<p class="font-medium text-red-700 dark:text-red-300">{{this.error}}</p>
</div>
{{else if this.refund}}
<div class="overflow-hidden rounded-2xl border border-gray-200 bg-white shadow-sm dark:border-gray-700 dark:bg-gray-800">
<div class="bg-indigo-600 px-8 py-5">
<p class="mb-1 text-xs font-semibold uppercase tracking-widest text-indigo-200">GNU Taler Refund</p>
<h1 class="text-2xl font-bold text-white">{{format-currency this.refund.amount this.refund.currency}}</h1>
</div>

<div class="space-y-5 px-8 py-6">
<div>
<p class="text-sm font-semibold text-gray-900 dark:text-white">Open this refund with your GNU Taler wallet.</p>
<p class="mt-1 text-sm text-gray-600 dark:text-gray-300">
This refund is for invoice
{{n-a this.refund.invoice.number}}.
If your wallet does not open automatically, scan the QR code or copy the wallet URI.
</p>
</div>

{{#if this.qrImageSrc}}
<div class="flex justify-center">
<div class="h-48 w-48 rounded-lg border border-gray-200 bg-white p-3 dark:border-gray-700">
<img src={{this.qrImageSrc}} alt="GNU Taler refund QR code" class="h-full w-full object-contain" />
</div>
</div>
{{/if}}

<div class="flex min-w-0 items-center justify-between gap-3 rounded-md border border-gray-200 bg-gray-50 px-3 py-2 dark:border-gray-700 dark:bg-gray-900/40">
<ClickToCopy @value={{this.talerRefundUri}} class="min-w-0 flex-1">
<span class="block truncate font-mono text-xs text-blue-600 dark:text-blue-400">{{this.talerRefundUri}}</span>
</ClickToCopy>
<ClickToCopy @value={{this.talerRefundUri}} class="flex-shrink-0">
<span class="text-xs font-semibold text-blue-600 hover:text-blue-700 dark:text-blue-400">Copy</span>
</ClickToCopy>
</div>

<div class="flex items-center justify-end gap-3">
<Button @icon="copy" @text="Copy URI" @size="sm" @type="default" @onClick={{this.copyRefundUri}} />
<a
href={{this.talerRefundUri}}
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>
</div>
{{/if}}
</div>
</div>
118 changes: 118 additions & 0 deletions addon/components/customer-taler-refund.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import Component from '@glimmer/component';
import { action } from '@ember/object';
import { inject as service } from '@ember/service';
import { tracked } from '@glimmer/tracking';
import { task } from 'ember-concurrency';

export default class CustomerTalerRefundComponent extends Component {
@service fetch;
@service notifications;
@service urlSearchParams;

@tracked refund = null;
@tracked error = null;

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

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

get refundId() {
return this.urlSearchParams.get('id');
}

get talerRefundUri() {
return this.refund?.taler_refund_uri;
}

get qrImageSrc() {
const qrImage = this.refund?.qr_image;

if (typeof qrImage !== 'string' || qrImage.length === 0) {
return null;
}

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

return `data:image/png;base64,${qrImage}`;
}

@task({ restartable: true })
*loadRefund() {
this.error = null;

if (!this.refundId) {
this.error = 'No refund identifier provided. Please check the link and try again.';
return;
}

try {
const result = yield this.fetch.get(`refunds/${this.refundId}`, {}, { namespace: 'ledger/public' });
this.refund = result?.refund ?? result;
this.updateTalerUriMeta();
} catch (error) {
const status = error?.status ?? error?.response?.status;
this.error = status === 404 ? 'Refund not found. Please check the link and try again.' : error?.message ?? 'Failed to load refund. Please try again later.';
}
}

@action copyRefundUri() {
if (!this.talerRefundUri) {
return;
}

navigator.clipboard?.writeText(this.talerRefundUri);
this.notifications.success('Refund URI copied.');
}

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

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

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

support.content = 'uri';
this.updateTalerUriMeta();
}

updateTalerUriMeta() {
if (typeof document === 'undefined' || !this.talerRefundUri) {
return;
}

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

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

uri.content = this.talerRefundUri;
}

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

document.querySelectorAll('meta[data-ledger-taler-support="true"]').forEach((meta) => meta.remove());
}
}
2 changes: 1 addition & 1 deletion addon/components/gateway/details.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@
{{/if}}
</div>
<Button
class="w-full whitespace-nowrap md:w-44 md:flex-shrink-0"
class="inline-flex flex-shrink-0 whitespace-nowrap"
@size="sm"
@icon={{gatewayAction.icon}}
@iconSpin={{gatewayAction.isLoading}}
Expand Down
12 changes: 11 additions & 1 deletion addon/components/gateway/form.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,17 @@
</div>
</div>

<Button @icon="plug" @text={{if (eq this.connectionState "success") "Test Again" "Test Credentials"}} @size="xs" @isLoading={{this.testCredentials.isRunning}} @disabled={{or this.testCredentials.isRunning (not @resource.id)}} @onClick={{perform this.testCredentials}} />
<div class="flex flex-shrink-0 justify-end">
<Button
@icon="plug"
@text={{if (eq this.connectionState "success") "Test Again" "Test Credentials"}}
@size="xs"
@isLoading={{this.testCredentials.isRunning}}
@disabled={{or this.testCredentials.isRunning (not this.canTestCredentials)}}
@onClick={{perform this.testCredentials}}
class="whitespace-nowrap"
/>
</div>
</div>
</div>
</div>
Expand Down
Loading
Loading