Skip to content

Commit

Permalink
add payments list
Browse files Browse the repository at this point in the history
  • Loading branch information
anbraten committed Nov 6, 2023
1 parent c699b99 commit 7ae6c77
Show file tree
Hide file tree
Showing 12 changed files with 275 additions and 1 deletion.
1 change: 1 addition & 0 deletions packages/app/components/Menu.vue
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
<MenuItem to="/customers" title="Customers" icon="i-ion-people" />
<MenuItem to="/subscriptions" title="Subscriptions" icon="i-ion-md-refresh" />
<MenuItem to="/invoices" title="Invoices" icon="i-ion-document-text" />
<MenuItem to="/payments" title="Payments" icon="i-ion-card" />
<MenuItem to="/project/settings" title="Settings" icon="i-ion-settings-sharp" />
<!-- <MenuItem to="/docs" title="Api docs" icon="i-ion-document-text-sharp" /> -->
<MenuItem to="https://geprog.com" title="Geprog" icon="i-ion-android-favorite-outline" />
Expand Down
13 changes: 13 additions & 0 deletions packages/app/components/status/Payment.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<template>
<UBadge v-if="payment.status === 'processing'" size="xs" label="Processing" color="amber" variant="subtle" />
<UBadge v-else-if="payment.status === 'paid'" size="xs" label="Paid" color="emerald" variant="subtle" />
<UBadge v-else-if="payment.status === 'failed'" size="xs" label="Failed" color="rose" variant="subtle" />
</template>

<script lang="ts" setup>
import type { Payment } from '@geprog/gringotts-client';
defineProps<{
payment: Payment;
}>();
</script>
63 changes: 63 additions & 0 deletions packages/app/pages/payments/[paymentId]/index.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
<template>
<div v-if="payment" class="w-full flex flex-col gap-4 max-w-4xl mx-auto">
<div class="flex justify-between">
<h1 class="text-xl">Payment: {{ payment._id }}</h1>

<StatusPayment :payment="payment" />
</div>

<UCard>
<div class="flex justify-end mb-2 gap-2 items-center">
<router-link v-if="payment.invoice" :to="`/invoices/${payment.invoice._id}`">
<UButton label="Invoice" icon="i-ion-document-text" size="sm" />
</router-link>
<router-link v-if="payment.customer" :to="`/customers/${payment.customer._id}`">
<UButton :label="payment.customer.name" icon="i-ion-people" size="sm" />
</router-link>
<router-link v-if="payment.subscription" :to="`/subscriptions/${payment.subscription._id}`">
<UButton label="Subscription" icon="i-ion-md-refresh" size="sm" />
</router-link>
</div>

<UForm :state="payment" class="flex flex-col gap-4">
<UFormGroup label="Description" name="description">
<UInput color="primary" variant="outline" v-model="payment.description" size="lg" disabled />
</UFormGroup>

<UFormGroup label="Type" name="type">
<UInput color="primary" variant="outline" v-model="payment.type" size="lg" disabled />
</UFormGroup>

<UFormGroup label="Amount" name="amount">
<UInput color="primary" variant="outline" v-model="payment._id" size="lg" disabled>
<template #trailing>
<span class="text-gray-500 dark:text-gray-400 text-xs">{{ payment.currency }}</span>
</template>
</UInput>
</UFormGroup>

<UFormGroup label="Status" name="status">
<USelectMenu
color="primary"
variant="outline"
v-model="payment.status"
:options="['active', 'error']"
size="lg"
disabled
/>
</UFormGroup>
</UForm>
</UCard>
</div>
</template>

<script lang="ts" setup>
const client = await useGringottsClient();
const route = useRoute();
const paymentId = route.params.paymentId as string;
const { data: payment } = useAsyncData(async () => {
const { data } = await client.payment.getPayment(paymentId);
return data;
});
</script>
57 changes: 57 additions & 0 deletions packages/app/pages/payments/index.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<template>
<div class="w-full">
<h1 class="text-xl">Payments</h1>

<UTable :loading="pending" :rows="payments || []" :columns="paymentColumns" @select="selectPayment">
<template #customer-data="{ row }">
<span>{{ row.customer.name }}</span>
</template>

<template #amount-data="{ row }">
<span>{{ formatCurrency(row.amount, row.currency) }}</span>
</template>

<template #status-data="{ row }">
<StatusPayment :payment="row" />
</template>
</UTable>
</div>
</template>

<script lang="ts" setup>
import type { Payment } from '@geprog/gringotts-client';
const router = useRouter();
const client = await useGringottsClient();
const paymentColumns = [
{
key: '_id',
label: 'ID',
},
{
key: 'description',
label: 'Description',
sortable: true,
},
{
key: 'status',
label: 'Status',
sortable: true,
},
{
key: 'amount',
label: 'Current period',
sortable: true,
},
];
async function selectPayment(row: Payment) {
await router.push(`/payments/${row._id}`);
}
const { data: payments, pending } = useAsyncData(async () => {
const { data } = await client.payment.listPayments();
return data;
});
</script>
1 change: 1 addition & 0 deletions packages/server/src/api/endpoints/payment.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ describe('Payment webhook endpoints', () => {
status: 'processing',
type: 'verification',
description: 'Verification payment',
project: testData.project,
});

const persistAndFlush = vi.fn();
Expand Down
66 changes: 66 additions & 0 deletions packages/server/src/api/endpoints/payment.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,77 @@
import { FastifyInstance } from 'fastify';

import { getProjectFromRequest } from '~/api/helpers';
import { database } from '~/database';
import { getPaymentProvider } from '~/payment_providers';
import { triggerWebhook } from '~/webhook';

// eslint-disable-next-line @typescript-eslint/require-await
export async function paymentEndpoints(server: FastifyInstance): Promise<void> {
server.get('/payment', {
schema: {
operationId: 'listPayments',
summary: 'List payments',
tags: ['payment'],
response: {
200: {
type: 'array',
items: {
$ref: 'Payment',
},
},
},
},
handler: async (request, reply) => {
const project = await getProjectFromRequest(request);

const payments = await database.payments.find({ project }, { populate: ['customer', 'invoice', 'subscription'] });

await reply.send(payments.map((i) => i.toJSON()));
},
});

server.get('/payment/:paymentId', {
schema: {
operationId: 'getPayment',
summary: 'Get a payment',
tags: ['payment'],
params: {
type: 'object',
required: ['paymentId'],
additionalProperties: false,
properties: {
paymentId: { type: 'string' },
},
},
response: {
200: {
$ref: 'Payment',
},
404: {
$ref: 'ErrorResponse',
},
},
},
handler: async (request, reply) => {
const project = await getProjectFromRequest(request);

const { paymentId } = request.params as { paymentId: string };
if (!paymentId) {
return reply.code(400).send({ error: 'Missing paymentId' });
}

const payment = await database.payments.findOne(
{ _id: paymentId, project },
{ populate: ['customer', 'invoice'] },
);
if (!payment) {
return reply.code(404).send({ error: 'Payment not found' });
}

await reply.send(payment.toJSON());
},
});

server.post('/payment/webhook/:projectId', {
schema: { hide: true },
handler: async (request, reply) => {
Expand Down
1 change: 1 addition & 0 deletions packages/server/src/api/endpoints/payment_method.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ export async function paymentMethodEndpoints(server: FastifyInstance): Promise<v
type: 'verification',
customer,
status: 'processing',
project,
});

const { checkoutUrl } = await paymentProvider.chargeForegroundPayment({
Expand Down
5 changes: 4 additions & 1 deletion packages/server/src/api/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,10 @@ export function addSchemas(server: FastifyInstance): void {
status: { type: 'string' },
currency: { type: 'string' },
amount: { type: 'number' },
description: { type: 'number' },
description: { type: 'string' },
invoice: { type: 'object', properties: { _id: { type: 'string' } }, additionalProperties: false },
customer: { $ref: 'Customer' },
subscription: { $ref: 'Subscription' },
},
});

Expand Down
5 changes: 5 additions & 0 deletions packages/server/src/database.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import { MigrationSetNextPaymentForSubscriptions } from '~/migrations/002_set_ne
import { MigrationPaymentStatusFromPendingToProcessing } from '~/migrations/003_update_payment_status';
import { MigrationUpdateInvoiceAddCustomerAndAllowOptionalSubscription } from '~/migrations/004_update_invoice_add_customer_make_subscription_optional';
import { MigrationReplaceNextPaymentWithCurrentPeriod } from '~/migrations/005_update_status_optional_invoice_subscription';
import { MigrationSetPaymentProject } from '~/migrations/006_set_payment_project';

export class Database {
orm!: MikroORM;
Expand Down Expand Up @@ -77,6 +78,10 @@ export class Database {
name: 'MigrationReplaceNextPaymentWithCurrentPeriod',
class: MigrationReplaceNextPaymentWithCurrentPeriod,
},
{
name: 'MigrationSetPaymentProject',
class: MigrationSetPaymentProject,
},
],
disableForeignKeys: false,
},
Expand Down
13 changes: 13 additions & 0 deletions packages/server/src/entities/payment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,19 @@ export class Payment {
constructor(data?: Partial<Payment>) {
Object.assign(this, data);
}

toJSON(): Payment {
return {
...this,
subscription: this.subscription?.toJSON(),
customer: this.customer.toJSON(),
invoice: this.invoice
? {
_id: this.invoice?._id,
}
: undefined,
};
}
}

export const paymentSchema = new EntitySchema<Payment>({
Expand Down
1 change: 1 addition & 0 deletions packages/server/src/loop.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ export async function chargeCustomerInvoice(invoice: Invoice): Promise<void> {
status: 'processing',
description: paymentDescription,
subscription: invoice.subscription,
project,
});

invoice.payment = payment;
Expand Down
50 changes: 50 additions & 0 deletions packages/server/src/migrations/006_set_payment_project.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { Migration } from '@mikro-orm/migrations';

type Payment = {
_id: string;
customer__id: string;
project__id: string;
};

type Customer = {
_id: string;
project__id: string;
};

export class MigrationSetPaymentProject extends Migration {
async up(): Promise<void> {
if (await this.ctx?.schema.hasColumn('payment', 'project__id')) {
return;
}

await this.ctx?.schema.alterTable('payment', (table) => {
table.uuid('project__id').nullable();
});

const payments = await this.ctx?.table<Payment>('payment').select<Payment[]>();
for await (const payment of payments || []) {
const customer = await this.ctx
?.table<Customer>('customer')
.where({ _id: payment.customer__id })
.first<Customer>();

await this.ctx?.table<Payment>('payment').where({ _id: payment._id }).update({
project__id: customer?.project__id,
});
}

await this.ctx?.schema.alterTable('payment', (table) => {
table.dropNullable('project__id');
});
}

async down(): Promise<void> {
if (!(await this.ctx?.schema.hasColumn('payment', 'project__id'))) {
return;
}

await this.ctx?.schema.alterTable('payment', (table) => {
table.dropColumn('project__id');
});
}
}

0 comments on commit 7ae6c77

Please sign in to comment.