Skip to content

Commit

Permalink
fix: reset active payment method and use correct invoice amount (#64)
Browse files Browse the repository at this point in the history
  • Loading branch information
anbraten authored Nov 6, 2023
1 parent a7ff5a2 commit 81e7f91
Showing 8 changed files with 66 additions and 11 deletions.
19 changes: 18 additions & 1 deletion packages/app/pages/subscriptions/[subscriptionId]/index.vue
Original file line number Diff line number Diff line change
@@ -11,6 +11,10 @@
size="sm"
@click="resetError"
/>

<router-link v-if="subscription.customer" :to="`/customers/${subscription.customer._id}`">
<UButton :label="subscription.customer.name" icon="i-ion-people" size="sm" />
</router-link>
</div>

<UForm :state="subscription" class="flex flex-col gap-4">
@@ -49,10 +53,14 @@
/>
</UFormGroup>

<UFormGroup v-if="subscription.error" label="Error" name="error">
<UFormGroup label="Error" name="error">
<UTextarea color="primary" variant="outline" v-model="subscription.error" size="lg" disabled />
</UFormGroup>

<UFormGroup label="Metadata" name="metadata">
<UTextarea color="primary" variant="outline" v-model="metadata" size="lg" disabled />
</UFormGroup>

<!-- <UButton label="Save" type="submit" class="mx-auto" /> -->
</UForm>
</UCard>
@@ -108,6 +116,15 @@ const { data: subscription, refresh } = useAsyncData(async () => {
return data;
});
const metadata = computed({
get() {
return subscription.value?.metadata ? JSON.stringify(subscription.value.metadata, null, 2) : '';
},
set(metadata: string) {
// TODO
},
});
const subscriptionChangeColumns = [
{
key: 'start',
14 changes: 9 additions & 5 deletions packages/server/src/api/endpoints/payment_method.test.ts
Original file line number Diff line number Diff line change
@@ -186,12 +186,13 @@ describe('Payment-method endpoints', () => {
paymentProviderId: '123',
};

const deleteMock = vi.fn();
const persistAndFlush = vi.fn();
const removeAndFlush = vi.fn();

vi.spyOn(database, 'database', 'get').mockReturnValue({
paymentMethods: {
findOne() {
return Promise.resolve(new PaymentMethod(paymentMethodData));
return Promise.resolve(new PaymentMethod({ ...paymentMethodData, customer: testData.customer }));
},
},
projects: {
@@ -200,7 +201,8 @@ describe('Payment-method endpoints', () => {
},
},
em: {
removeAndFlush: deleteMock,
persistAndFlush,
removeAndFlush,
},
} as unknown as database.Database);

@@ -221,7 +223,9 @@ describe('Payment-method endpoints', () => {
const paymentMethodResponse: { ok: boolean } = response.json();
expect(paymentMethodResponse).toBeDefined();
expect(paymentMethodResponse).toStrictEqual({ ok: true });
expect(deleteMock).toBeCalledTimes(1);
expect(deleteMock).toHaveBeenCalledWith(paymentMethodData);
expect(removeAndFlush).toBeCalledTimes(1);
expect(removeAndFlush).toHaveBeenCalledWith(paymentMethodData);

expect(persistAndFlush).toBeCalledTimes(1);
});
});
10 changes: 9 additions & 1 deletion packages/server/src/api/endpoints/payment_method.ts
Original file line number Diff line number Diff line change
@@ -205,11 +205,19 @@ export async function paymentMethodEndpoints(server: FastifyInstance): Promise<v

const { paymentMethodId } = request.params as { paymentMethodId: string };

const paymentMethod = await database.paymentMethods.findOne({ _id: paymentMethodId, project });
const paymentMethod = await database.paymentMethods.findOne(
{ _id: paymentMethodId, project },
{ populate: ['customer'] },
);
if (!paymentMethod) {
return reply.code(404).send({ error: 'Payment-method not found' });
}

if (paymentMethod.customer.activePaymentMethod?._id === paymentMethod._id) {
paymentMethod.customer.activePaymentMethod = undefined;
await database.em.persistAndFlush(paymentMethod.customer);
}

await database.em.removeAndFlush(paymentMethod);

await reply.send({ ok: true });
6 changes: 6 additions & 0 deletions packages/server/src/api/endpoints/subscription.ts
Original file line number Diff line number Diff line change
@@ -20,6 +20,7 @@ export async function subscriptionEndpoints(server: FastifyInstance): Promise<vo
pricePerUnit: { type: 'number' },
units: { type: 'number' },
customerId: { type: 'string' },
metadata: { type: 'object' },
},
},
response: {
@@ -45,6 +46,7 @@ export async function subscriptionEndpoints(server: FastifyInstance): Promise<vo
units: number;
redirectUrl: string;
customerId: string;
metadata?: Subscription['metadata'];
};

if (body.units < 1) {
@@ -85,6 +87,7 @@ export async function subscriptionEndpoints(server: FastifyInstance): Promise<vo
currentPeriodEnd: currentPeriod.end,
customer,
project,
metadata: body.metadata,
});

subscription.changePlan({ units: body.units, pricePerUnit: body.pricePerUnit });
@@ -150,6 +153,7 @@ export async function subscriptionEndpoints(server: FastifyInstance): Promise<vo
units: { type: 'number' },
status: { type: 'string' },
error: { type: 'string' },
metadata: { type: 'object' },
},
},
response: {
@@ -182,6 +186,7 @@ export async function subscriptionEndpoints(server: FastifyInstance): Promise<vo
units?: number;
error?: string;
status?: Subscription['status'];
metadata?: Subscription['metadata'];
};

if (body.units !== undefined && body.pricePerUnit !== undefined) {
@@ -202,6 +207,7 @@ export async function subscriptionEndpoints(server: FastifyInstance): Promise<vo

subscription.error = body.error ?? subscription.error;
subscription.status = body.status ?? subscription.status;
subscription.metadata = body.metadata ?? subscription.metadata;

await database.em.persistAndFlush(subscription);

1 change: 1 addition & 0 deletions packages/server/src/api/schema.ts
Original file line number Diff line number Diff line change
@@ -136,6 +136,7 @@ export function addSchemas(server: FastifyInstance): void {
type: 'object',
properties: {
_id: { type: 'string' },
metadata: { type: 'object', additionalProperties: true },
anchorDate: { type: 'string' },
status: { type: 'string' },
error: { type: 'string' },
2 changes: 2 additions & 0 deletions packages/server/src/entities/subscription.ts
Original file line number Diff line number Diff line change
@@ -10,6 +10,7 @@ import { SubscriptionPeriod } from '~/entities/subscription_period';

export class Subscription {
_id: string = v4();
metadata?: Record<string, unknown>;
anchorDate!: Date; // first date a user ever started a subscription for the object
status: 'processing' | 'active' | 'error' = 'active';
error?: string;
@@ -84,6 +85,7 @@ export const subscriptionSchema = new EntitySchema<Subscription>({
class: Subscription,
properties: {
_id: { type: 'uuid', onCreate: () => v4(), primary: true },
metadata: { type: 'json', nullable: true },
anchorDate: { type: Date },
status: { type: 'string', default: 'active' },
error: { type: 'string', nullable: true },
22 changes: 19 additions & 3 deletions packages/server/src/loop.test.ts
Original file line number Diff line number Diff line change
@@ -206,13 +206,17 @@ describe('Loop', () => {

// then
expect(persistAndFlush).toBeCalledTimes(2);
const [[updatedInvoice]] = persistAndFlush.mock.calls[1] as [[Invoice]];
const [[updatedInvoice, payment]] = persistAndFlush.mock.calls[1] as [[Invoice, Payment]];
expect(updatedInvoice).toBeDefined();
expect(updatedInvoice.items.getItems().find((i) => i.description === 'Credit')).toBeDefined();
expect(updatedInvoice.amount).toStrictEqual(Invoice.roundPrice(invoiceAmount - balance));
expect(updatedInvoice.totalAmount).toStrictEqual(
Invoice.roundPrice((invoiceAmount - balance) * (1 + invoice.vatRate / 100)),
);

expect(payment).toBeDefined();
expect(payment.amount).toStrictEqual(updatedInvoice.totalAmount);
expect(payment.status).toStrictEqual('processing');
});

it('should apply customer balance when charging and keep remaining balance', async () => {
@@ -242,20 +246,32 @@ describe('Loop', () => {
customer.balance = balance;
const invoiceAmount = invoice.amount;

const now = dayjs('2021-01-01').toDate();
vi.setSystemTime(now);

// when
await chargeCustomerInvoice(invoice);

// then
expect(persistAndFlush).toBeCalledTimes(2);
expect(persistAndFlush).toBeCalledTimes(3);

const [[updatedCustomer]] = persistAndFlush.mock.calls[0] as [[Customer]];
expect(updatedCustomer).toBeDefined();
expect(updatedCustomer.balance).toStrictEqual(balance - invoiceAmount);

const [[updatedInvoice]] = persistAndFlush.mock.calls[1] as [[Invoice]];
const [[updatedSubscription]] = persistAndFlush.mock.calls[1] as [[Subscription]];
expect(updatedSubscription).toBeDefined();
expect(updatedSubscription.lastPayment).toStrictEqual(now);

const [[updatedInvoice, payment]] = persistAndFlush.mock.calls[2] as [[Invoice, Payment]];
expect(updatedInvoice).toBeDefined();
expect(updatedInvoice.items.getItems().find((i) => i.description === 'Credit')).toBeDefined();
expect(updatedInvoice.amount).toStrictEqual(0);
expect(updatedInvoice.totalAmount).toStrictEqual(0);
expect(updatedInvoice.status).toStrictEqual('paid');

expect(payment).toBeDefined();
expect(payment.amount).toStrictEqual(updatedInvoice.totalAmount);
expect(payment.status).toStrictEqual('paid');
});
});
3 changes: 2 additions & 1 deletion packages/server/src/loop.ts
Original file line number Diff line number Diff line change
@@ -20,7 +20,7 @@ export async function chargeCustomerInvoice(invoice: Invoice): Promise<void> {
throw new Error('Customer has no active payment method');
}

const amount = Invoice.roundPrice(invoice.totalAmount);
let amount = Invoice.roundPrice(invoice.totalAmount);

// add customer credit if amount is above 0 and customer has credit
if (customer.balance > 0 && amount > 0) {
@@ -34,6 +34,7 @@ export async function chargeCustomerInvoice(invoice: Invoice): Promise<void> {
);
customer.balance = Invoice.roundPrice(customer.balance - creditAmount);
await database.em.persistAndFlush([customer]);
amount = Invoice.roundPrice(invoice.totalAmount); // update amount from updated invoice
log.debug({ customerId: customer._id, creditAmount }, 'Credit applied');
}

0 comments on commit 81e7f91

Please sign in to comment.