mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-16 04:40:32 +00:00
feat: Stripe connect using OAuth
This commit is contained in:
@@ -13,6 +13,13 @@ export class StripeIntegrationController extends BaseController {
|
||||
public router() {
|
||||
const router = Router();
|
||||
|
||||
router.get('/link', this.getStripeConnectLink.bind(this));
|
||||
router.post(
|
||||
'/callback',
|
||||
[body('code').exists()],
|
||||
this.validationResult,
|
||||
this.exchangeOAuth.bind(this)
|
||||
);
|
||||
router.post('/account', asyncMiddleware(this.createAccount.bind(this)));
|
||||
router.post(
|
||||
'/account_link',
|
||||
@@ -27,6 +34,47 @@ export class StripeIntegrationController extends BaseController {
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves Stripe OAuth2 connect link.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
* @returns {Promise<Response|void>}
|
||||
*/
|
||||
public async getStripeConnectLink(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
try {
|
||||
const authorizationUri = this.stripePaymentApp.getStripeConnectLink();
|
||||
|
||||
return res.status(200).send({ url: authorizationUri });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Exchanges the given Stripe authorization code to Stripe user id and access token.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
public async exchangeOAuth(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId } = req;
|
||||
const { code } = this.matchedBodyData(req);
|
||||
|
||||
try {
|
||||
await this.stripePaymentApp.exchangeStripeOAuthToken(tenantId, code);
|
||||
|
||||
return res.status(200).send({});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a Stripe checkout session for the given payment link id.
|
||||
* @param {Request} req
|
||||
|
||||
@@ -268,6 +268,8 @@ module.exports = {
|
||||
stripePayment: {
|
||||
secretKey: process.env.STRIPE_PAYMENT_SECRET_KEY || '',
|
||||
publishableKey: process.env.STRIPE_PAYMENT_PUBLISHABLE_KEY || '',
|
||||
clientId: process.env.STRIPE_PAYMENT_CLIENT_ID || '',
|
||||
redirectTo: process.env.STRIPE_PAYMENT_REDIRECT_URL || '',
|
||||
webhooksSecret: process.env.STRIPE_PAYMENT_WEBHOOKS_SECRET || '',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -8,7 +8,8 @@ exports.up = function (knex) {
|
||||
table.string('service');
|
||||
table.string('name');
|
||||
table.string('slug');
|
||||
table.boolean('active').defaultTo(false);
|
||||
table.boolean('payment_enabled').defaultTo(false);
|
||||
table.boolean('payout_enabled').defaultTo(false);
|
||||
table.string('account_id');
|
||||
table.json('options');
|
||||
table.timestamps();
|
||||
|
||||
@@ -31,6 +31,17 @@ export const PrepardExpenses = {
|
||||
predefined: true,
|
||||
};
|
||||
|
||||
export const StripeClearingAccount = {
|
||||
name: 'Stripe Clearing',
|
||||
slug: 'stripe-clearing',
|
||||
account_type: 'other-current-liability',
|
||||
parent_account_id: null,
|
||||
code: '50006',
|
||||
active: true,
|
||||
index: 1,
|
||||
predefined: true,
|
||||
}
|
||||
|
||||
export default [
|
||||
{
|
||||
name: 'Bank Account',
|
||||
|
||||
@@ -120,6 +120,7 @@ import { SeedInitialDemoAccountDataOnOrgBuild } from '@/services/OneClickDemo/ev
|
||||
import { EventsTrackerListeners } from '@/services/EventsTracker/events/events';
|
||||
import { InvoicePaymentIntegrationSubscriber } from '@/services/Sales/Invoices/subscribers/InvoicePaymentIntegrationSubscriber';
|
||||
import { StripeWebhooksSubscriber } from '@/services/StripePayment/events/StripeWebhooksSubscriber';
|
||||
import { SeedStripeAccountsOnOAuthGrantedSubscriber } from '@/services/StripePayment/events/SeedStripeAccounts';
|
||||
|
||||
export default () => {
|
||||
return new EventPublisher();
|
||||
@@ -294,6 +295,7 @@ export const susbcribers = () => {
|
||||
// Stripe Payment
|
||||
InvoicePaymentIntegrationSubscriber,
|
||||
StripeWebhooksSubscriber,
|
||||
SeedStripeAccountsOnOAuthGrantedSubscriber,
|
||||
|
||||
...EventsTrackerListeners
|
||||
];
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { Model } from 'objection';
|
||||
import TenantModel from 'models/TenantModel';
|
||||
|
||||
export class PaymentIntegration extends TenantModel {
|
||||
export class PaymentIntegration extends Model {
|
||||
paymentEnabled!: boolean;
|
||||
payoutEnabled!: boolean;
|
||||
|
||||
static get tableName() {
|
||||
return 'payment_integrations';
|
||||
}
|
||||
@@ -10,16 +13,35 @@ export class PaymentIntegration extends TenantModel {
|
||||
return 'id';
|
||||
}
|
||||
|
||||
static get virtualAttributes() {
|
||||
return ['fullEnabled'];
|
||||
}
|
||||
|
||||
static get jsonAttributes() {
|
||||
return ['options'];
|
||||
}
|
||||
|
||||
get fullEnabled() {
|
||||
return this.paymentEnabled && this.payoutEnabled;
|
||||
}
|
||||
|
||||
static get jsonSchema() {
|
||||
return {
|
||||
type: 'object',
|
||||
required: ['name', 'service', 'active'],
|
||||
required: ['name', 'service'],
|
||||
properties: {
|
||||
id: { type: 'integer' },
|
||||
service: { type: 'string' },
|
||||
active: { type: 'boolean' },
|
||||
paymentEnabled: { type: 'boolean' },
|
||||
payoutEnabled: { type: 'boolean' },
|
||||
accountId: { type: 'string' },
|
||||
options: { type: 'object' },
|
||||
options: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
bankAccountId: { type: 'number' },
|
||||
clearingAccountId: { type: 'number' },
|
||||
},
|
||||
},
|
||||
createdAt: { type: 'string', format: 'date-time' },
|
||||
updatedAt: { type: 'string', format: 'date-time' },
|
||||
},
|
||||
|
||||
@@ -4,6 +4,7 @@ import { IAccount } from '@/interfaces';
|
||||
import { Knex } from 'knex';
|
||||
import {
|
||||
PrepardExpenses,
|
||||
StripeClearingAccount,
|
||||
TaxPayableAccount,
|
||||
UnearnedRevenueAccount,
|
||||
} from '@/database/seeds/data/accounts';
|
||||
@@ -247,4 +248,37 @@ export default class AccountRepository extends TenantRepository {
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Finds or creates the stripe clearing account.
|
||||
* @param {Record<string, string>} extraAttrs
|
||||
* @param {Knex.Transaction} trx
|
||||
* @returns
|
||||
*/
|
||||
public async findOrCreateStripeClearing(
|
||||
extraAttrs: Record<string, string> = {},
|
||||
trx?: Knex.Transaction
|
||||
) {
|
||||
// Retrieves the given tenant metadata.
|
||||
const tenantMeta = await TenantMetadata.query().findOne({
|
||||
tenantId: this.tenantId,
|
||||
});
|
||||
const _extraAttrs = {
|
||||
currencyCode: tenantMeta.baseCurrency,
|
||||
...extraAttrs,
|
||||
};
|
||||
|
||||
let result = await this.model
|
||||
.query(trx)
|
||||
.findOne({ slug: StripeClearingAccount.slug, ..._extraAttrs });
|
||||
|
||||
if (!result) {
|
||||
result = await this.model.query(trx).insertAndFetch({
|
||||
...StripeClearingAccount,
|
||||
..._extraAttrs,
|
||||
});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,12 +3,16 @@ import HasTenancyService from '../Tenancy/TenancyService';
|
||||
import { GetPaymentMethodsPOJO } from './types';
|
||||
import config from '@/config';
|
||||
import { isStripePaymentConfigured } from './utils';
|
||||
import { GetStripeAuthorizationLinkService } from '../StripePayment/GetStripeAuthorizationLink';
|
||||
|
||||
@Service()
|
||||
export class GetPaymentMethodsStateService {
|
||||
@Inject()
|
||||
private tenancy: HasTenancyService;
|
||||
|
||||
@Inject()
|
||||
private getStripeAuthorizationLinkService: GetStripeAuthorizationLinkService;
|
||||
|
||||
/**
|
||||
* Retrieves the payment state provising state.
|
||||
* @param {number} tenantId
|
||||
@@ -25,7 +29,9 @@ export class GetPaymentMethodsStateService {
|
||||
service: 'Stripe',
|
||||
});
|
||||
const isStripeAccountCreated = !!stripePayment;
|
||||
const isStripePaymentActive = !!(stripePayment?.active || null);
|
||||
const isStripePaymentEnabled = stripePayment?.paymentEnabled;
|
||||
const isStripePayoutEnabled = stripePayment?.payoutEnabled;
|
||||
const isStripeEnabled = stripePayment?.fullEnabled;
|
||||
|
||||
const stripePaymentMethodId = stripePayment?.id || null;
|
||||
const stripeAccountId = stripePayment?.accountId || null;
|
||||
@@ -33,16 +39,21 @@ export class GetPaymentMethodsStateService {
|
||||
const stripeCurrencies = ['USD', 'EUR'];
|
||||
const stripeRedirectUrl = 'https://your-stripe-redirect-url.com';
|
||||
const isStripeServerConfigured = isStripePaymentConfigured();
|
||||
const stripeAuthLink =
|
||||
this.getStripeAuthorizationLinkService.getStripeAuthLink();
|
||||
|
||||
const paymentMethodPOJO: GetPaymentMethodsPOJO = {
|
||||
stripe: {
|
||||
isStripeAccountCreated,
|
||||
isStripePaymentActive,
|
||||
isStripePaymentEnabled,
|
||||
isStripePayoutEnabled,
|
||||
isStripeEnabled,
|
||||
isStripeServerConfigured,
|
||||
stripeAccountId,
|
||||
stripePaymentMethodId,
|
||||
stripePublishableKey,
|
||||
stripeCurrencies,
|
||||
stripeAuthLink,
|
||||
stripeRedirectUrl,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -16,11 +16,17 @@ export interface EditPaymentMethodDTO {
|
||||
export interface GetPaymentMethodsPOJO {
|
||||
stripe: {
|
||||
isStripeAccountCreated: boolean;
|
||||
isStripePaymentActive: boolean;
|
||||
|
||||
isStripePaymentEnabled: boolean;
|
||||
isStripePayoutEnabled: boolean;
|
||||
isStripeEnabled: boolean;
|
||||
|
||||
isStripeServerConfigured: boolean;
|
||||
|
||||
stripeAccountId: string | null;
|
||||
stripePaymentMethodId: number | null;
|
||||
stripePublishableKey: string | null;
|
||||
stripeAuthLink: string;
|
||||
stripeCurrencies: Array<string>;
|
||||
stripeRedirectUrl: string | null;
|
||||
};
|
||||
|
||||
@@ -35,7 +35,8 @@ export class GetSaleInvoice {
|
||||
.withGraphFetched('customer')
|
||||
.withGraphFetched('branch')
|
||||
.withGraphFetched('taxes.taxRate')
|
||||
.withGraphFetched('attachments');
|
||||
.withGraphFetched('attachments')
|
||||
.withGraphFetched('paymentMethods');
|
||||
|
||||
// Validates the given sale invoice existance.
|
||||
this.validators.validateInvoiceExistance(saleInvoice);
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { StripePaymentService } from './StripePaymentService';
|
||||
import events from '@/subscribers/events';
|
||||
import HasTenancyService from '../Tenancy/TenancyService';
|
||||
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
|
||||
import UnitOfWork from '../UnitOfWork';
|
||||
import { Knex } from 'knex';
|
||||
import { StripeOAuthCodeGrantedEventPayload } from './types';
|
||||
|
||||
@Service()
|
||||
export class ExchangeStripeOAuthTokenService {
|
||||
@Inject()
|
||||
private stripePaymentService: StripePaymentService;
|
||||
|
||||
@Inject()
|
||||
private tenancy: HasTenancyService;
|
||||
|
||||
@Inject()
|
||||
private eventPublisher: EventPublisher;
|
||||
|
||||
@Inject()
|
||||
private uow: UnitOfWork;
|
||||
|
||||
/**
|
||||
* Exchange stripe oauth authorization code to access token and user id.
|
||||
* @param {number} tenantId
|
||||
* @param {string} authorizationCode
|
||||
*/
|
||||
public async excahngeStripeOAuthToken(
|
||||
tenantId: number,
|
||||
authorizationCode: string
|
||||
) {
|
||||
const { PaymentIntegration } = this.tenancy.models(tenantId);
|
||||
const stripe = this.stripePaymentService.stripe;
|
||||
|
||||
const response = await stripe.oauth.token({
|
||||
grant_type: 'authorization_code',
|
||||
code: authorizationCode,
|
||||
});
|
||||
// const accessToken = response.access_token;
|
||||
// const refreshToken = response.refresh_token;
|
||||
const stripeUserId = response.stripe_user_id;
|
||||
|
||||
// Retrieves details of the Stripe account.
|
||||
const account = await stripe.accounts.retrieve(stripeUserId, {
|
||||
expand: ['business_profile'],
|
||||
});
|
||||
const companyName = account.business_profile?.name || 'Unknow name';
|
||||
const paymentEnabled = account.charges_enabled;
|
||||
const payoutEnabled = account.payouts_enabled;
|
||||
|
||||
//
|
||||
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
|
||||
// Stores the details of the Stripe account.
|
||||
const paymentIntegration = await PaymentIntegration.query(trx).insert({
|
||||
name: companyName,
|
||||
service: 'Stripe',
|
||||
accountId: stripeUserId,
|
||||
paymentEnabled,
|
||||
payoutEnabled,
|
||||
});
|
||||
// Triggers `onStripeOAuthCodeGranted` event.
|
||||
await this.eventPublisher.emitAsync(
|
||||
events.stripeIntegration.onOAuthCodeGranted,
|
||||
{
|
||||
tenantId,
|
||||
paymentIntegrationId: paymentIntegration.id,
|
||||
trx,
|
||||
} as StripeOAuthCodeGrantedEventPayload
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { Service } from 'typedi';
|
||||
import config from '@/config';
|
||||
|
||||
@Service()
|
||||
export class GetStripeAuthorizationLinkService {
|
||||
public getStripeAuthLink() {
|
||||
const clientId = config.stripePayment.clientId;
|
||||
const redirectUrl = config.stripePayment.redirectTo;
|
||||
|
||||
const authorizationUri = `https://connect.stripe.com/oauth/v2/authorize?response_type=code&client_id=${clientId}&scope=read_write&redirect_uri=${redirectUrl}`;
|
||||
|
||||
return authorizationUri;
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,8 @@ import { StripeInvoiceCheckoutSessionPOJO } from '@/interfaces/StripePayment';
|
||||
import { CreateStripeAccountService } from './CreateStripeAccountService';
|
||||
import { CreateStripeAccountLinkService } from './CreateStripeAccountLink';
|
||||
import { CreateStripeAccountDTO } from './types';
|
||||
import { ExchangeStripeOAuthTokenService } from './ExchangeStripeOauthToken';
|
||||
import { GetStripeAuthorizationLinkService } from './GetStripeAuthorizationLink';
|
||||
|
||||
export class StripePaymentApplication {
|
||||
@Inject()
|
||||
@@ -15,6 +17,12 @@ export class StripePaymentApplication {
|
||||
@Inject()
|
||||
private createInvoiceCheckoutSessionService: CreateInvoiceCheckoutSession;
|
||||
|
||||
@Inject()
|
||||
private exchangeStripeOAuthTokenService: ExchangeStripeOAuthTokenService;
|
||||
|
||||
@Inject()
|
||||
private getStripeConnectLinkService: GetStripeAuthorizationLinkService;
|
||||
|
||||
/**
|
||||
* Creates a new Stripe account for Bigcapital.
|
||||
* @param {number} tenantId
|
||||
@@ -58,4 +66,24 @@ export class StripePaymentApplication {
|
||||
paymentLinkId
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves Stripe OAuth2 connect link.
|
||||
* @returns {string}
|
||||
*/
|
||||
public getStripeConnectLink() {
|
||||
return this.getStripeConnectLinkService.getStripeAuthLink();
|
||||
}
|
||||
|
||||
/**
|
||||
* Exchanges the given Stripe authorization code to Stripe user id and access token.
|
||||
* @param {string} authorizationCode
|
||||
* @returns
|
||||
*/
|
||||
public exchangeStripeOAuthToken(tenantId: number, authorizationCode: string) {
|
||||
return this.exchangeStripeOAuthTokenService.excahngeStripeOAuthToken(
|
||||
tenantId,
|
||||
authorizationCode
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,11 +2,11 @@ import { Service } from 'typedi';
|
||||
import stripe from 'stripe';
|
||||
import config from '@/config';
|
||||
|
||||
const origin = 'http://localhost:4000';
|
||||
const origin = 'https://cfdf-102-164-97-88.ngrok-free.app';
|
||||
|
||||
@Service()
|
||||
export class StripePaymentService {
|
||||
public stripe;
|
||||
public stripe: stripe;
|
||||
|
||||
constructor() {
|
||||
this.stripe = new stripe(config.stripePayment.secretKey, {
|
||||
@@ -36,9 +36,9 @@ export class StripePaymentService {
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {number} accountId
|
||||
* @returns
|
||||
*
|
||||
* @param {number} accountId
|
||||
* @returns
|
||||
*/
|
||||
public async createAccountLink(accountId: string) {
|
||||
try {
|
||||
@@ -62,8 +62,9 @@ export class StripePaymentService {
|
||||
*/
|
||||
public async createAccount(): Promise<string> {
|
||||
try {
|
||||
const account = await this.stripe.accounts.create({});
|
||||
|
||||
const account = await this.stripe.accounts.create({
|
||||
type: 'standard',
|
||||
});
|
||||
return account;
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export const STRIPE_PAYMENT_LINK_REDIRECT = 'https://your_redirect_url.com';
|
||||
@@ -0,0 +1,49 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import events from '@/subscribers/events';
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
import { StripeOAuthCodeGrantedEventPayload } from '../types';
|
||||
|
||||
@Service()
|
||||
export class SeedStripeAccountsOnOAuthGrantedSubscriber {
|
||||
@Inject()
|
||||
private tenancy: HasTenancyService;
|
||||
|
||||
/**
|
||||
* Attaches the subscriber to the event dispatcher.
|
||||
*/
|
||||
public attach(bus) {
|
||||
bus.subscribe(
|
||||
events.stripeIntegration.onOAuthCodeGranted,
|
||||
this.handleSeedStripeAccount.bind(this)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Seeds the default integration settings once oauth authorization code granted.
|
||||
* @param {StripeCheckoutSessionCompletedEventPayload} payload -
|
||||
*/
|
||||
async handleSeedStripeAccount({
|
||||
tenantId,
|
||||
paymentIntegrationId,
|
||||
trx,
|
||||
}: StripeOAuthCodeGrantedEventPayload) {
|
||||
const { PaymentIntegration } = this.tenancy.models(tenantId);
|
||||
const { accountRepository } = this.tenancy.repositories(tenantId);
|
||||
|
||||
const clearingAccount = await accountRepository.findOrCreateStripeClearing(
|
||||
{},
|
||||
trx
|
||||
);
|
||||
const bankAccount = await accountRepository.findBySlug('bank-account');
|
||||
|
||||
// Patch the Stripe integration default settings.
|
||||
await PaymentIntegration.query(trx)
|
||||
.findById(paymentIntegrationId)
|
||||
.patch({
|
||||
options: {
|
||||
bankAccountId: bankAccount.id,
|
||||
clearingAccountId: clearingAccount.id,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,11 @@
|
||||
|
||||
import { Knex } from 'knex';
|
||||
|
||||
|
||||
export interface CreateStripeAccountDTO {
|
||||
name?: string;
|
||||
}
|
||||
export interface StripeOAuthCodeGrantedEventPayload {
|
||||
tenantId: number;
|
||||
paymentIntegrationId: number;
|
||||
trx?: Knex.Transaction
|
||||
}
|
||||
@@ -723,7 +723,9 @@ export default {
|
||||
onAccountDeleted: 'onStripeIntegrationAccountDeleted',
|
||||
|
||||
onPaymentLinkCreated: 'onStripePaymentLinkCreated',
|
||||
onPaymentLinkInactivated: 'onStripePaymentLinkInactivated'
|
||||
onPaymentLinkInactivated: 'onStripePaymentLinkInactivated',
|
||||
|
||||
onOAuthCodeGranted: 'onStripeOAuthCodeGranted',
|
||||
},
|
||||
|
||||
// Stripe Payment Webhooks
|
||||
|
||||
Reference in New Issue
Block a user