mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-17 21:30:31 +00:00
feat: wip Stripe connect integration
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
import { StripePaymentService } from '@/services/StripePayment/StripePaymentService';
|
||||
import { NextFunction, Request, Response, Router } from 'express';
|
||||
import { Inject, Service } from 'typedi';
|
||||
import config from '@/config';
|
||||
import bodyParser from 'body-parser';
|
||||
import { SaleInvoiceStripePaymentLink } from '@/services/StripePayment/SaleInvoiceStripePaymentLink';
|
||||
import { CreatePaymentReceiveStripePayment } from '@/services/StripePayment/CreatePaymentReceivedStripePayment';
|
||||
|
||||
@Service()
|
||||
export class StripeWebhooksController {
|
||||
@Inject()
|
||||
private stripePaymentService: StripePaymentService;
|
||||
|
||||
@Inject()
|
||||
private createPaymentReceiveStripePayment: CreatePaymentReceiveStripePayment;
|
||||
|
||||
router() {
|
||||
const router = Router();
|
||||
|
||||
router.post(
|
||||
'/stripe',
|
||||
bodyParser.raw({ type: 'application/json' }),
|
||||
this.handleWebhook.bind(this)
|
||||
);
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param req
|
||||
* @param res
|
||||
* @param next
|
||||
*/
|
||||
public async handleWebhook(req: Request, res: Response, next: NextFunction) {
|
||||
try {
|
||||
let event = req.body;
|
||||
const sig = req.headers['stripe-signature'];
|
||||
|
||||
// Verify webhook signature and extract the event.
|
||||
// See https://stripe.com/docs/webhooks#verify-events for more information.
|
||||
try {
|
||||
event = this.stripePaymentService.stripe.webhooks.constructEvent(
|
||||
req.rawBody,
|
||||
sig,
|
||||
config.stripePayment.webhooksSecret
|
||||
);
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
return res.status(400).send(`Webhook Error: ${err.message}`);
|
||||
}
|
||||
// Handle the event based on its type
|
||||
switch (event.type) {
|
||||
case 'checkout.session.completed':
|
||||
const { metadata } = event.data.object;
|
||||
const tenantId = parseInt(metadata.tenantId, 10);
|
||||
const saleInvoiceId = parseInt(metadata.saleInvoiceId, 10);
|
||||
|
||||
// Get the amount from the event
|
||||
const amount = event.data.object.amount_total;
|
||||
|
||||
// Convert from Stripe amount (cents) to normal amount (dollars)
|
||||
const amountInDollars = amount / 100;
|
||||
|
||||
await this.createPaymentReceiveStripePayment.createPaymentReceived(
|
||||
tenantId,
|
||||
saleInvoiceId,
|
||||
amountInDollars
|
||||
);
|
||||
break;
|
||||
case 'payment_intent.payment_failed':
|
||||
// Handle failed payment intent
|
||||
console.log('PaymentIntent failed.');
|
||||
break;
|
||||
// Add more cases as needed
|
||||
default:
|
||||
console.log(`Unhandled event type ${event.type}`);
|
||||
}
|
||||
|
||||
res.status(200).json({ received: true });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,10 @@
|
||||
import { NextFunction, Router, Request, Response } from 'express';
|
||||
import { Inject, Service } from 'typedi';
|
||||
import Container, { Inject, Service } from 'typedi';
|
||||
import { PlaidApplication } from '@/services/Banking/Plaid/PlaidApplication';
|
||||
import BaseController from '../BaseController';
|
||||
import { LemonSqueezyWebhooks } from '@/services/Subscription/LemonSqueezyWebhooks';
|
||||
import { PlaidWebhookTenantBootMiddleware } from '@/services/Banking/Plaid/PlaidWebhookTenantBootMiddleware';
|
||||
import { StripeWebhooksController } from '../StripeIntegration/StripeWebhooksController';
|
||||
|
||||
@Service()
|
||||
export class Webhooks extends BaseController {
|
||||
@@ -24,6 +25,8 @@ export class Webhooks extends BaseController {
|
||||
|
||||
router.post('/lemon', this.lemonWebhooks.bind(this));
|
||||
|
||||
router.use(Container.get(StripeWebhooksController).router());
|
||||
|
||||
return router;
|
||||
}
|
||||
|
||||
|
||||
@@ -268,5 +268,6 @@ module.exports = {
|
||||
stripePayment: {
|
||||
secretKey: process.env.STRIPE_PAYMENT_SECRET_KEY || '',
|
||||
publishableKey: process.env.STRIPE_PAYMENT_PUBLISHABLE_KEY || '',
|
||||
webhooksSecret: process.env.STRIPE_PAYMENT_WEBHOOKS_SECRET || '',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* @param { import("knex").Knex } knex
|
||||
* @returns { Promise<void> }
|
||||
*/
|
||||
exports.up = function (knex) {
|
||||
return knex.schema.table('sales_invoices', (table) => {
|
||||
table.string('stripe_plink_id').nullable();
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* @param { import("knex").Knex } knex
|
||||
* @returns { Promise<void> }
|
||||
*/
|
||||
exports.down = function (knex) {
|
||||
return knex.schema.table('sales_invoices', (table) => {
|
||||
table.dropColumn('stripe_plink_id');
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* @param { import("knex").Knex } knex
|
||||
* @returns { Promise<void> }
|
||||
*/
|
||||
exports.up = function (knex) {
|
||||
return knex.schema.table('payment_receives', (table) => {
|
||||
table.string('stripe_pintent_id').nullable();
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* @param { import("knex").Knex } knex
|
||||
* @returns { Promise<void> }
|
||||
*/
|
||||
exports.down = function (knex) {
|
||||
return knex.schema.table('payment_receives', (table) => {
|
||||
table.dropColumn('stripe_pintent_id');
|
||||
});
|
||||
};
|
||||
@@ -119,6 +119,7 @@ import { DeleteUncategorizedTransactionsOnAccountDeleting } from '@/services/Ban
|
||||
import { SeedInitialDemoAccountDataOnOrgBuild } from '@/services/OneClickDemo/events/SeedInitialDemoAccountData';
|
||||
import { TriggerInvalidateCacheOnSubscriptionChange } from '@/services/Subscription/events/TriggerInvalidateCacheOnSubscriptionChange';
|
||||
import { EventsTrackerListeners } from '@/services/EventsTracker/events/events';
|
||||
import { CreatePaymentLinkOnInvoiceCreated } from '@/services/StripePayment/events/CreatePaymentLinkOnInvoiceCreated';
|
||||
|
||||
export default () => {
|
||||
return new EventPublisher();
|
||||
@@ -291,6 +292,9 @@ export const susbcribers = () => {
|
||||
// Demo Account
|
||||
SeedInitialDemoAccountDataOnOrgBuild,
|
||||
|
||||
// Stripe Payment
|
||||
CreatePaymentLinkOnInvoiceCreated
|
||||
|
||||
...EventsTrackerListeners
|
||||
];
|
||||
};
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { GetSaleInvoice } from '../Sales/Invoices/GetSaleInvoice';
|
||||
import { CreatePaymentReceived } from '../Sales/PaymentReceived/CreatePaymentReceived';
|
||||
|
||||
@Service()
|
||||
export class CreatePaymentReceiveStripePayment {
|
||||
@Inject()
|
||||
private getSaleInvoiceService: GetSaleInvoice;
|
||||
|
||||
@Inject()
|
||||
private createPaymentReceivedService: CreatePaymentReceived;
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {number} tenantId
|
||||
* @param {number} saleInvoiceId
|
||||
* @param {number} paidAmount
|
||||
*/
|
||||
async createPaymentReceived(
|
||||
tenantId: number,
|
||||
saleInvoiceId: number,
|
||||
paidAmount: number
|
||||
) {
|
||||
const invoice = await this.getSaleInvoiceService.getSaleInvoice(
|
||||
tenantId,
|
||||
saleInvoiceId
|
||||
);
|
||||
await this.createPaymentReceivedService.createPaymentReceived(tenantId, {
|
||||
customerId: invoice.customerId,
|
||||
paymentDate: new Date(),
|
||||
amount: paidAmount,
|
||||
depositAccountId: 1002,
|
||||
statement: '',
|
||||
entries: [{ invoiceId: saleInvoiceId, paymentAmount: paidAmount }],
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import HasTenancyService from '../Tenancy/TenancyService';
|
||||
import { StripePaymentService } from './StripePaymentService';
|
||||
import { Knex } from 'knex';
|
||||
|
||||
@Service()
|
||||
export class DeleteStripePaymentLinkInvoice {
|
||||
@Inject()
|
||||
private tenancy: HasTenancyService;
|
||||
|
||||
@Inject()
|
||||
private stripePayment: StripePaymentService;
|
||||
|
||||
/**
|
||||
* Deletes the Stripe payment link associates to the given sale invoice.
|
||||
* @param {number} tenantId
|
||||
* @param {number} invoiceId
|
||||
*/
|
||||
async deletePaymentLink(
|
||||
tenantId: number,
|
||||
invoiceId: number,
|
||||
trx?: Knex.Transaction
|
||||
): Promise<void> {
|
||||
const { SaleInvoice } = this.tenancy.models(tenantId);
|
||||
const invoice = await SaleInvoice.query().findById(invoiceId);
|
||||
|
||||
const stripeAcocunt = { stripeAccount: 'acct_1Px3dSPjeOqFxnPw' };
|
||||
|
||||
if (invoice.stripePlinkId) {
|
||||
await this.stripePayment.stripe.paymentLinks.update(
|
||||
invoice.stripePlinkId,
|
||||
{ active: false },
|
||||
stripeAcocunt
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import { ISaleInvoice } from '@/interfaces';
|
||||
import { StripePaymentService } from './StripePaymentService';
|
||||
import { Inject, Service } from 'typedi';
|
||||
import HasTenancyService from '../Tenancy/TenancyService';
|
||||
import { STRIPE_PAYMENT_LINK_REDIRECT } from './constants';
|
||||
|
||||
@Service()
|
||||
export class SaleInvoiceStripePaymentLink {
|
||||
@Inject()
|
||||
private stripePayment: StripePaymentService;
|
||||
|
||||
@Inject()
|
||||
private tenancy: HasTenancyService;
|
||||
|
||||
/**
|
||||
* Creates a Stripe payment link for the given sale invoice.
|
||||
* @param {number} tenantId
|
||||
* @param {ISaleInvoice} saleInvoice
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
async createPaymentLink(tenantId: number, saleInvoice: ISaleInvoice) {
|
||||
const { SaleInvoice } = this.tenancy.models(tenantId);
|
||||
const saleInvoiceId = saleInvoice.id;
|
||||
|
||||
try {
|
||||
const stripeAcocunt = { stripeAccount: 'acct_1Px3dSPjeOqFxnPw' };
|
||||
const price = await this.stripePayment.stripe.prices.create(
|
||||
{
|
||||
unit_amount: saleInvoice.total * 100,
|
||||
currency: 'usd',
|
||||
product_data: {
|
||||
name: saleInvoice.invoiceNo,
|
||||
},
|
||||
},
|
||||
stripeAcocunt
|
||||
);
|
||||
const paymentLinkInfo = {
|
||||
line_items: [{ price: price.id, quantity: 1 }],
|
||||
after_completion: {
|
||||
type: 'redirect',
|
||||
redirect: {
|
||||
url: STRIPE_PAYMENT_LINK_REDIRECT,
|
||||
},
|
||||
},
|
||||
metadata: { saleInvoiceId, tenantId, resource: 'SaleInvoice' },
|
||||
};
|
||||
const paymentLink = await this.stripePayment.stripe.paymentLinks.create(
|
||||
paymentLinkInfo,
|
||||
stripeAcocunt
|
||||
);
|
||||
await SaleInvoice.query().findById(saleInvoiceId).patch({
|
||||
stripePlinkId: paymentLink.id,
|
||||
});
|
||||
return paymentLink.id;
|
||||
} catch (error) {
|
||||
console.error('Error creating payment link:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import config from '@/config';
|
||||
|
||||
@Service()
|
||||
export class StripePaymentService {
|
||||
private stripe;
|
||||
public stripe;
|
||||
|
||||
constructor() {
|
||||
this.stripe = new stripe(config.stripePayment.secretKey, {
|
||||
@@ -12,7 +12,12 @@ export class StripePaymentService {
|
||||
});
|
||||
}
|
||||
|
||||
public async createAccountSession(accountId: string) {
|
||||
/**
|
||||
*
|
||||
* @param {number} accountId
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
public async createAccountSession(accountId: string): Promise<string> {
|
||||
try {
|
||||
const accountSession = await this.stripe.accountSessions.create({
|
||||
account: accountId,
|
||||
@@ -28,7 +33,7 @@ export class StripePaymentService {
|
||||
}
|
||||
}
|
||||
|
||||
public async createAccount() {
|
||||
public async createAccount(): Promise<string> {
|
||||
try {
|
||||
const account = await this.stripe.accounts.create({});
|
||||
|
||||
|
||||
1
packages/server/src/services/StripePayment/constants.ts
Normal file
1
packages/server/src/services/StripePayment/constants.ts
Normal file
@@ -0,0 +1 @@
|
||||
export const STRIPE_PAYMENT_LINK_REDIRECT = 'https://your_redirect_url.com';
|
||||
@@ -0,0 +1,66 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { EventSubscriber } from '@/lib/EventPublisher/EventPublisher';
|
||||
import {
|
||||
ISaleInvoiceCreatedPayload,
|
||||
ISaleInvoiceDeletedPayload,
|
||||
} from '@/interfaces';
|
||||
import { SaleInvoiceStripePaymentLink } from '../SaleInvoiceStripePaymentLink';
|
||||
import { runAfterTransaction } from '@/services/UnitOfWork/TransactionsHooks';
|
||||
import events from '@/subscribers/events';
|
||||
import { DeleteStripePaymentLinkInvoice } from '../DeleteStripePaymentLinkInvoice';
|
||||
|
||||
@Service()
|
||||
export class CreatePaymentLinkOnInvoiceCreated extends EventSubscriber {
|
||||
@Inject()
|
||||
private invoiceStripePaymentLink: SaleInvoiceStripePaymentLink;
|
||||
|
||||
@Inject()
|
||||
private deleteStripePaymentLinkInvoice: DeleteStripePaymentLinkInvoice;
|
||||
|
||||
/**
|
||||
* Constructor method.
|
||||
*/
|
||||
public attach(bus) {
|
||||
bus.subscribe(
|
||||
events.saleInvoice.onCreated,
|
||||
this.handleUpdateTransactionsOnItemCreated
|
||||
);
|
||||
bus.subscribe(
|
||||
events.saleInvoice.onDeleted,
|
||||
this.handleDeletePaymentLinkOnInvoiceDeleted
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the Plaid item transactions
|
||||
* @param {IPlaidItemCreatedEventPayload} payload - Event payload.
|
||||
*/
|
||||
private handleUpdateTransactionsOnItemCreated = async ({
|
||||
saleInvoice,
|
||||
saleInvoiceId,
|
||||
tenantId,
|
||||
trx,
|
||||
}: ISaleInvoiceCreatedPayload) => {
|
||||
runAfterTransaction(trx, async () => {
|
||||
await this.invoiceStripePaymentLink.createPaymentLink(
|
||||
tenantId,
|
||||
saleInvoice
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Deletes the Stripe payment link once the associated invoice deleted.
|
||||
* @param {ISaleInvoiceDeletedPayload}
|
||||
*/
|
||||
private handleDeletePaymentLinkOnInvoiceDeleted = async ({
|
||||
saleInvoiceId,
|
||||
tenantId,
|
||||
}: ISaleInvoiceDeletedPayload) => {
|
||||
await this.deleteStripePaymentLinkInvoice.deletePaymentLink(
|
||||
tenantId,
|
||||
saleInvoiceId
|
||||
);
|
||||
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* @param { import("knex").Knex } knex
|
||||
* @returns { Promise<void> }
|
||||
*/
|
||||
exports.up = function (knex) {
|
||||
return knex.schema.createTable('stripe_accounts', (table) => {
|
||||
table.increments('id').primary();
|
||||
table.string('stripe_account_id').notNullable();
|
||||
table.string('tenant_id').notNullable();
|
||||
table.timestamps(true, true); // Adds created_at and updated_at columns
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* @param { import("knex").Knex } knex
|
||||
* @returns { Promise<void> }
|
||||
*/
|
||||
exports.down = function (knex) {
|
||||
return knex.schema.dropTableIfExists('stripe_accounts');
|
||||
};
|
||||
49
packages/server/src/system/models/StripeAccount.ts
Normal file
49
packages/server/src/system/models/StripeAccount.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { Model } from 'objection';
|
||||
|
||||
export class StripeAccount {
|
||||
/**
|
||||
* Table name
|
||||
*/
|
||||
static get tableName() {
|
||||
return 'stripe_accounts';
|
||||
}
|
||||
|
||||
/**
|
||||
* Timestamps columns.
|
||||
*/
|
||||
get timestamps() {
|
||||
return ['createdAt', 'updatedAt'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Virtual attributes.
|
||||
*/
|
||||
static get virtualAttributes() {
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Model modifiers.
|
||||
*/
|
||||
static get modifiers() {
|
||||
return {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Relationship mapping.
|
||||
*/
|
||||
static get relationMappings() {
|
||||
const Tenant = require('./Tenant');
|
||||
|
||||
return {
|
||||
tenant: {
|
||||
relation: Model.BelongsToOneRelation,
|
||||
modelClass: Tenant.default,
|
||||
join: {
|
||||
from: 'stripe_accounts.tenant_id',
|
||||
to: 'tenants.id',
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import PasswordReset from './PasswordReset';
|
||||
import Invite from './Invite';
|
||||
import SystemPlaidItem from './SystemPlaidItem';
|
||||
import { Import } from './Import';
|
||||
import { StripeAccount } from './StripeAccount';
|
||||
|
||||
export {
|
||||
Plan,
|
||||
@@ -18,4 +19,5 @@ export {
|
||||
Invite,
|
||||
SystemPlaidItem,
|
||||
Import,
|
||||
StripeAccount
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user