diff --git a/.env.dist b/.env.dist new file mode 100644 index 0000000..434e1f3 --- /dev/null +++ b/.env.dist @@ -0,0 +1,3 @@ +BRAINTREE_MERCHANT_ID=merchantId +BRAINTREE_PUBLIC_KEY=publicKey +BRAINTREE_PRIVATE_KEY=privateKey \ No newline at end of file diff --git a/.gitignore b/.gitignore index 0e75fe5..9e5db7c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ node_modules dist coverage +.env \ No newline at end of file diff --git a/README.md b/README.md index 5b932dc..7a24ff0 100644 --- a/README.md +++ b/README.md @@ -86,6 +86,55 @@ export default { }; ``` +## Transactions + +Braintree is capable of making one off transactions + +```typescript +import { Module } from '@nestjs/common'; +import { BraintreeModule, InjectBraintreeProvider } from 'nestjs-braintree'; +import { ConfigModule, ConfigService } from 'nestjs-config'; + +class TransactionProvider { + constructor( + @InjectBraintreeProvider() + private readonly braintreeProvider: BraintreeProvider, + ) {} + + takePayment(amount: string, nonce: string) { + this.braintreeProvider.sale({ + payment_method_nonce: nonce, + amount, + }); + } +} + +@Module({ + imports: [ + ConfigModule.load('root/to/config/*/**.{ts,js}'), + BraintreeModule.forRoot({ + useFactory: async (config: ConfigService) => config.get('braintree'), + inject: [ConfigService], + }), + ], + providers: [TransactionProvider], +}) +export default class AppModule {} +``` + +Avaliable methods relating to transactions are + +#### Sale +`braintreeProvider.sale(transaction: BraintreeTransactionInterface): Promise` + +#### Refund +`braintreeProvider.refund(transactionId: string, amount?: string, orderId?: string): Promise` + +#### Find +`braintreeProvider.find(transactionId: string): Promise` + +> The braintree SDK does offer additional methods. I will implement them soon hopefully + ## Webhooks When using subscriptions with braintree, braintree will issue webhooks to your @@ -197,37 +246,3 @@ export default class AppModule {} ``` The above will result in your route for your braintree webhooks being `{your_domain}/replace-braintree/replace-webhook` - -## Transactions - -Braintree is also capable of making one off transactions - -```typescript -import { Module } from '@nestjs/common'; -import { BraintreeModule, InjectBraintreeProvider } from 'nestjs-braintree'; -import { ConfigModule, ConfigService } from 'nestjs-config'; - -class TransactionProvider { - constructor( - @InjectBraintreeProvider() - private readonly braintreeProvider: BraintreeProvider, - ) {} - - takePayment(amount: number) { - //Will probably be similar to sale https://developers.braintreepayments.com/guides/transactions/node#settlement - this.braintreeProvider.notImplementedYet(amount); - } -} - -@Module({ - imports: [ - ConfigModule.load('root/to/config/*/**.{ts,js}'), - BraintreeModule.forRoot({ - useFactory: async (config: ConfigService) => config.get('braintree'), - inject: [ConfigService], - }), - ], - providers: [TransactionProvider], -}) -export default class AppModule {} -``` diff --git a/src/__tests__/__stubs__/config/braintree.ts b/src/__tests__/__stubs__/config/braintree.ts index 047ab94..d8552cb 100644 --- a/src/__tests__/__stubs__/config/braintree.ts +++ b/src/__tests__/__stubs__/config/braintree.ts @@ -2,7 +2,7 @@ import * as braintree from 'braintree'; export default { environment: braintree.Environment.Sandbox, - merchantId: 'merchantId', - publicKey: 'publicKey', - privateKey: 'privateKey', + merchantId: process.env.BRAINTREE_MERCHANT_ID, + publicKey: process.env.BRAINTREE_PUBLIC_KEY, + privateKey: process.env.BRAINTREE_PRIVATE_KEY, } \ No newline at end of file diff --git a/src/__tests__/braintree.module.spec.ts b/src/__tests__/braintree.module.spec.ts index 74bd67c..e8113d6 100644 --- a/src/__tests__/braintree.module.spec.ts +++ b/src/__tests__/braintree.module.spec.ts @@ -50,9 +50,9 @@ describe('Braintree Module', () => { const provider = module.get(BraintreeProvider); expect(options.environment).toBe(braintree.Environment.Sandbox); - expect(options.merchantId).toBe('merchantId'); - expect(options.publicKey).toBe('publicKey'); - expect(options.privateKey).toBe('privateKey'); + expect(typeof options.merchantId).toBe('string'); + expect(typeof options.publicKey).toBe('string'); + expect(typeof options.privateKey).toBe('string'); expect(provider).toBeInstanceOf(BraintreeProvider); }); diff --git a/src/__tests__/braintree.subscription.spec.ts b/src/__tests__/braintree.subscription.spec.ts new file mode 100644 index 0000000..27c8ae1 --- /dev/null +++ b/src/__tests__/braintree.subscription.spec.ts @@ -0,0 +1,37 @@ +import { TestingModule, Test } from '@nestjs/testing'; +import { ConfigModule, ConfigService } from 'nestjs-config'; +import * as path from 'path'; +import { BraintreeModule, BraintreeProvider } from './../../src'; + +describe('Braintree subscription methods', () => { + let module: TestingModule; + let provider: BraintreeProvider; + + beforeEach(async () => { + module = await Test.createTestingModule({ + imports: [ + ConfigModule.load( + path.resolve(__dirname, '__stubs__', 'config', '*.ts'), + ), + BraintreeModule.forRootAsync({ + useFactory: async config => config.get('braintree'), + inject: [ConfigService], + }), + ], + }).compile(); + + provider = module.get(BraintreeProvider); + }); + + it('', () => {}); + + // it('Create Subscription', async () => { + + // //TODO implement paymentMethodToken somehow. Try this https://developers.braintreepayments.com/reference/request/payment-method/create/node + + // // const result = await provider.createSubscription({ + // // paymentMethodToken: '', + // // planId: 'c8vr', + // // }); + // }); +}); diff --git a/src/__tests__/braintree.transactions.spec.ts b/src/__tests__/braintree.transactions.spec.ts new file mode 100644 index 0000000..7e48e83 --- /dev/null +++ b/src/__tests__/braintree.transactions.spec.ts @@ -0,0 +1,61 @@ +import { TestingModule, Test } from '@nestjs/testing'; +import { ConfigModule, ConfigService } from 'nestjs-config'; +import * as path from 'path'; +import { BraintreeModule, BraintreeProvider } from './../../src'; + +const nonces = { + valid: 'fake-valid-nonce', +}; + +describe('Braintree transaction methods', () => { + let module: TestingModule; + let provider: BraintreeProvider; + let transactionId: string; + + beforeEach(async () => { + module = await Test.createTestingModule({ + imports: [ + ConfigModule.load( + path.resolve(__dirname, '__stubs__', 'config', '*.ts'), + ), + BraintreeModule.forRootAsync({ + useFactory: async config => config.get('braintree'), + inject: [ConfigService], + }), + ], + }).compile(); + + provider = module.get(BraintreeProvider); + }); + + it('Create Transaction', async () => { + const result = await provider.sale({ + payment_method_nonce: nonces.valid, + amount: '10.00', + }); + + transactionId = result.transaction.id; + + expect(result.success).toBeTruthy(); + expect(result).toHaveProperty('transaction'); + expect(result.transaction).toHaveProperty('amount'); + expect(result.transaction.amount).toEqual('10.00'); + }); + + it('Find Transaction', async () => { + const transaction = await provider.find(transactionId); + + expect(transaction).toHaveProperty('id'); + expect(transaction).toHaveProperty('status'); + expect(transaction).toHaveProperty('type'); + expect(transaction).toHaveProperty('amount'); + expect(transaction).toHaveProperty('createdAt'); + expect(transaction).toHaveProperty('updatedAt'); + }); + + //it('Refund Transaction', async () => { + //const refundResult = await provider.refund(transactionId); + + //console.log('refund', refundResult); + //}); +}); diff --git a/src/__tests__/braintree.webhook.controller.options.spec.ts b/src/__tests__/braintree.webhook.controller.options.spec.ts index 8a8c5b6..a31f8b1 100644 --- a/src/__tests__/braintree.webhook.controller.options.spec.ts +++ b/src/__tests__/braintree.webhook.controller.options.spec.ts @@ -1,4 +1,4 @@ -import {Test, TestingModule} from '@nestjs/testing'; +import { Test, TestingModule } from '@nestjs/testing'; import BraintreeModule from '../braintree.module'; import BraintreeWebhookModule from '../braintree.webhook.module'; import * as braintree from 'braintree'; @@ -22,11 +22,29 @@ describe('BraintreeWebhookController', () => { const controller = module.get(BraintreeWebhookController); expect(controller).toBeInstanceOf(BraintreeWebhookController); - expect(Reflect.getMetadata(PATH_METADATA, BraintreeWebhookController)).toBe('braintree'); - expect(Reflect.getMetadata(METHOD_METADATA, Object.getOwnPropertyDescriptor(BraintreeWebhookController.prototype, 'handle').value)).toBe(RequestMethod.POST); - expect(Reflect.getMetadata(PATH_METADATA, Object.getOwnPropertyDescriptor(BraintreeWebhookController.prototype, 'handle').value)).toBe('webhook'); + expect(Reflect.getMetadata(PATH_METADATA, BraintreeWebhookController)).toBe( + 'braintree', + ); + expect( + Reflect.getMetadata( + METHOD_METADATA, + Object.getOwnPropertyDescriptor( + BraintreeWebhookController.prototype, + 'handle', + ).value, + ), + ).toBe(RequestMethod.POST); + expect( + Reflect.getMetadata( + PATH_METADATA, + Object.getOwnPropertyDescriptor( + BraintreeWebhookController.prototype, + 'handle', + ).value, + ), + ).toBe('webhook'); }); - + it('Should instance with forRoot', async () => { const module: TestingModule = await Test.createTestingModule({ imports: [ @@ -45,8 +63,26 @@ describe('BraintreeWebhookController', () => { const controller = module.get(BraintreeWebhookController); expect(controller).toBeInstanceOf(BraintreeWebhookController); - expect(Reflect.getMetadata(PATH_METADATA, BraintreeWebhookController)).toBe('testing'); - expect(Reflect.getMetadata(METHOD_METADATA, Object.getOwnPropertyDescriptor(BraintreeWebhookController.prototype, 'handle').value)).toBe(RequestMethod.POST); - expect(Reflect.getMetadata(PATH_METADATA, Object.getOwnPropertyDescriptor(BraintreeWebhookController.prototype, 'handle').value)).toBe('this'); + expect(Reflect.getMetadata(PATH_METADATA, BraintreeWebhookController)).toBe( + 'testing', + ); + expect( + Reflect.getMetadata( + METHOD_METADATA, + Object.getOwnPropertyDescriptor( + BraintreeWebhookController.prototype, + 'handle', + ).value, + ), + ).toBe(RequestMethod.POST); + expect( + Reflect.getMetadata( + PATH_METADATA, + Object.getOwnPropertyDescriptor( + BraintreeWebhookController.prototype, + 'handle', + ).value, + ), + ).toBe('this'); }); -}); \ No newline at end of file +}); diff --git a/src/__tests__/braintree.webhook.provider.spec.ts b/src/__tests__/braintree.webhook.provider.spec.ts index 5742748..a7de851 100644 --- a/src/__tests__/braintree.webhook.provider.spec.ts +++ b/src/__tests__/braintree.webhook.provider.spec.ts @@ -1,6 +1,4 @@ import { Test, TestingModule } from '@nestjs/testing'; -import * as path from 'path'; -import { ConfigModule, ConfigService } from 'nestjs-config'; import { BraintreeModule, BraintreeWebhookModule, @@ -33,12 +31,11 @@ describe('BraintreeWebhookController', async () => { const module: TestingModule = await Test.createTestingModule({ imports: [ - ConfigModule.load( - path.resolve(__dirname, '__stubs__', 'config', '*.ts'), - ), - BraintreeModule.forRootAsync({ - useFactory: async config => config.get('braintree'), - inject: [ConfigService], + BraintreeModule.forRoot({ + environment: braintree.Environment.Sandbox, + merchantId: 'merchantId', + publicKey: 'publicKey', + privateKey: 'privateKey', }), BraintreeWebhookModule, ], diff --git a/src/__tests__/e2e/braintree.controller.e2e-spec.ts b/src/__tests__/e2e/braintree.controller.e2e-spec.ts index de579cd..fe0dbd7 100644 --- a/src/__tests__/e2e/braintree.controller.e2e-spec.ts +++ b/src/__tests__/e2e/braintree.controller.e2e-spec.ts @@ -39,9 +39,11 @@ describe('BraintreeWebhookController', async () => { ConfigModule.load( path.resolve(__dirname, '../', '__stubs__', 'config', '*.ts'), ), - BraintreeModule.forRootAsync({ - useFactory: async config => config.get('braintree'), - inject: [ConfigService], + BraintreeModule.forRoot({ + environment: braintree.Environment.Sandbox, + merchantId: 'merchantId', + publicKey: 'publicKey', + privateKey: 'privateKey', }), BraintreeWebhookModule, ], diff --git a/src/braintree.provider.ts b/src/braintree.provider.ts index 0cc0fbb..7934ff3 100644 --- a/src/braintree.provider.ts +++ b/src/braintree.provider.ts @@ -1,5 +1,13 @@ import {Injectable, Inject} from '@nestjs/common'; -import { BraintreeOptions, BraintreeWebhookPayloadInterface, BraintreeWebhookNotificationInterface } from './interfaces'; +import { + BraintreeOptions, + BraintreeWebhookPayloadInterface, + BraintreeWebhookNotificationInterface, + BraintreeTransactionInterface, + BraintreeTransactionResultInterface, + BraintreeSubscriptionInterface, + BraintreeSubscriptionResultInterface, +} from './interfaces'; import * as braintree from 'braintree'; import { BRAINTREE_OPTIONS_PROVIDER } from './braintree.constants'; @@ -16,7 +24,35 @@ export default class BraintreeProvider { return await this.gateway.webhookNotification.parse(payload.bt_signature, payload.bt_payload); } - //TODO add methods to handle creating a subscription - //TODO add methods to handle transactions - //TODO add methods for refunds + async sale(transaction: BraintreeTransactionInterface): Promise { + + return await this.gateway.transaction.sale(transaction); + } + + async refund(transactionId: string, amount?: string, orderId?: string): Promise { + return await this.gateway.transaction.refund(transactionId, amount, orderId); + } + + async find(transactionId: string): Promise { + return await this.gateway.transaction.find(transactionId); + } + + async createSubscription(subscription: BraintreeSubscriptionInterface): Promise { + return await this.gateway.subscription.create(subscription); + } + + async cancelSubscription(subscriptionId: string): Promise { + return await this.gateway.subscription.cancel(subscriptionId); + } + + async findSubscription(subscriptionId: string): Promise { + return await this.gateway.subscription.find(subscriptionId); + } + + async updateSubscription(subscriptionId: string, subscription: BraintreeSubscriptionInterface): Promise { + return await this.gateway.subscription.update(subscriptionId, subscription); + } + + // TODO implement confusing looking search plans + // https://developers.braintreepayments.com/reference/request/subscription/search/node#search-results } \ No newline at end of file diff --git a/src/braintree.webhook.controller.ts b/src/braintree.webhook.controller.ts index a993c41..f67afce 100644 --- a/src/braintree.webhook.controller.ts +++ b/src/braintree.webhook.controller.ts @@ -1,4 +1,4 @@ -import {Controller, Req, Logger, HttpException, Post, RequestMethod, Provider} from '@nestjs/common'; +import {Controller, Req, Logger, HttpException, RequestMethod} from '@nestjs/common'; import BraintreeProvider from './braintree.provider'; import BraintreeWebhookProvider from './braintree.webhook.provider'; import { BraintreeWebhookNotificationInterface } from './interfaces'; @@ -29,10 +29,9 @@ export default class BraintreeWebhookController { async handle(@Req() request) { let webhook: BraintreeWebhookNotificationInterface; - try { - webhook = await this.braintree.parseWebhook({ - bt_signature: request.body.bt_signature, + webhook = await this.braintree.parseWebhook({ + bt_signature: request.body.bt_signature, bt_payload: request.body.bt_payload, }); await this.webhookProvider.handle(webhook); diff --git a/src/braintree.webhook.provider.ts b/src/braintree.webhook.provider.ts index e33633c..80b42b5 100644 --- a/src/braintree.webhook.provider.ts +++ b/src/braintree.webhook.provider.ts @@ -29,7 +29,6 @@ export default class BraintreeWebhookProvider { }; async handle(webhook: BraintreeWebhookNotificationInterface): Promise { - if (Object.keys(this.methods).includes(webhook.kind)) { this.methods[webhook.kind].forEach(async (methodProto: BraintreeWebhookMethodInterface) => { try { diff --git a/src/interfaces/braintree.transaction.interface.ts b/src/interfaces/braintree.transaction.interface.ts new file mode 100644 index 0000000..4825d73 --- /dev/null +++ b/src/interfaces/braintree.transaction.interface.ts @@ -0,0 +1,267 @@ +import { + CreditCard, + DisbursementDetails, + PayPalAccount, + ApplePayCard, + AndroidPayCard, + CoinbaseAccount, + VisaCheckoutCard, + MasterpassCard, + SamsungPayCard, +} from 'braintree'; + +export interface BraintreeTransactionOptionsInterface { + submitForSettlement?: boolean; + addBillingAddressToPaymentMethod?: boolean; + holdInEscrow?: boolean; + paypal?: BraintreePayPalOptionsInterface; + skipAdvancedFraudChecking?: boolean; + skipAvs?: boolean; + skipCvv?: boolean; + storeInVault?: boolean; + storeInVaultOnSuccess?: boolean; + storeShippingAddressInVault?: boolean; + treeDSecure?: { + required: boolean; + }; + venmo?: { + profileId: string; + }; +} + +export interface BraintreePayPalOptionsInterface { + customField: string; + description: string; +} + +export interface BraintreeDescriptorInterface { + name?: string; + phone?: string; + url?: string; +} + +export interface BraintreeAddressInterface { + company?: string; + countryName?: string; + countryCodeAlpha2?: string; + countryCodeAlpha3?: string; + countryCodeNumeric?: string; + extendedAddress?: string; + firstName?: string; + lastName?: string; + locality?: string; + postalCode?: string; + region?: string; + streetAddress?: string; +} + +export interface BraintreeCustomerInterface { + id?: string; + firstName?: string; + lastName?: string; + company?: string; + phone?: string; + fax?: string; + website?: string; + email?: string; +} + +export interface BraintreeTransactionInterface { + payment_method_nonce: string; + amount: string; + billing?: BraintreeAddressInterface; + billingAddressId?: string; + channel?: string; + customFields?: { [s: string]: string }; + customerId?: string; + deviceSessionId?: string; + discountAmount?: string; + merchantAccountId?: string; + options?: BraintreeTransactionInterface; + orderId?: string; + paymentMethodToken?: string; + purchaseOrderNumber?: string; + serviceFeeAmount?: string; + sharedBillingAddress?: string; + sharedCustomerId?: string; + sharedPaymentMethodNonce?: string; + sharedPaymentMethodToken?: string; + sharedSippingAddressId?: string; + shipping?: BraintreeAddressInterface; + shippingAddressId?: string; + shippingAmount?: string; + shippingFromPostalCode?: string; + taxAmount?: string; + taxExempt?: boolean; + transactionSource?: 'recurring' | 'unscheduled' | 'recurring_first' | 'moto'; + descriptor?: BraintreeDescriptorInterface; +} + +export interface BraintreeTransactionResultInterface { + success: boolean; + transaction: BraintreeTransaction; +} + +export interface BraintreeSubscriptionOptionsInterface { + doNotInheritAddOnsOrDiscounts?: boolean; + paypal?: { + description?: string; + }; + startImmediately?: boolean; +} + +export interface BraintreeSubscriptionInterface { + paymentMethodToken: string; + planId: string; + price?: string; + paymentMethodNonce?: string; + options?: BraintreeSubscriptionOptionsInterface; + trialDuration?: number; + trialDurationUnit?: 'day' | 'month'; + trialPeriod?: boolean; + numberOfBillingCycles?: number; + neverExpires?: boolean; + merchantAccountId?: string; + id?: string; + firstBillingDate?: Date; + discounts?: { + add?: { + amount?: string; + inheritFromId: string; + neverExpires?: boolean; + numberOfBillingCycles?: string; + quantity?: number; + }[]; + remove: string[]; + update: { + amount?: string; + existingId: string; + neverExpires?: boolean; + numberOfBillingCycles?: number; + quantity?: number; + }[]; + }; + descriptor?: BraintreeDescriptorInterface; +} + +export interface BraintreeTransaction { + id: string; + status: string; + type: string; + currencyIsoCode: string; + amount: string; + merchantAccountId: string; + subMerchantAccountId: string | null; + masterMerchantAccountId: string | null; + orderId: string | null; + createdAt: string; + updatedAt: string; + customer: { + id: string | null; + firstName: string | null; + lastName: string | null; + company: string | null; + email: string | null; + website: string | null; + phone: string | null; + fax: string | null; + }; + billing: { + id: string | null; + firstName: string | null; + lastName: string | null; + company: string | null; + streetAddress: string | null; + extendedAddress: string | null; + locality: string | null; + region: string | null; + postalCode: string | null; + countryName: string | null; + countryCodeAlpha2: string | null; + countryCodeAlpha3: string | null; + countryCodeNumeric: string | null; + }; + refundId: string | null; + refundIds: string[]; + refundedTransactionId: string; + partialSettlementTransactionIds: string[]; + authorizedTransactionId: string | null; + settlementBatchId: string | null; + shipping: { + id: string | null; + firstName: string | null; + lastName: string | null; + company: string | null; + streetAddress: string | null; + extendedAddress: string | null; + locality: string | null; + region: string | null; + postalCode: string | null; + countryName: string | null; + countryCodeAlpha2: string | null; + countryCodeAlpha3: string | null; + countryCodeNumeric: string | null; + }; + customFields: string | object; + avsErrorResponseCode: string | null; + avsPostalCodeResponseCode: string; + avsStreetAddressResponseCode: string; + cvvResponseCode: string; + gatewayRejectionReason: null | string; + processorAuthorizationCode: string; + processorResponseCode: string; + processorResponseText: string; + additionalProcessorResponse: null | string; + voiceReferralNumber: null | string; + purchaseOrderNumber: null | string; + taxAmount: null | string; + taxExempt: boolean; + creditCard: CreditCard; + statusHistory: object; + planId: null | string; + subscriptionId: null | string; + subscription: { + billingPeriodEndDate: null | string; + billingPeriodStartDate: null | string; + }; + addOns: []; + discounts: []; + descriptor: { + name: string | null; + phone: string | null; + url: string | null; + }; + recurring: boolean; + channel: null | string; + serviceFeeAmount: null | string; + escrowStatus: null | string; + disbursementDetails: DisbursementDetails; + disputes: []; + authorizationAjustments: []; + paymentInstrumentType: string; + processorSettlementResponseCode: string; + processorSettlementResponseText: string; + threeDSecureInfo: null | string; + shipsFromPostalCode: null | string; + shippingAmount: null | string; + discountAmount: null | string; + networkTransactionId: string; + refundGlobalIds: []; + refundedTransactionGlobalId: null | string; + partialSettlementTransactionGlobalIds: []; + authorizedTransactionGlobalId: null | string; + globalId: string; + processorResponseType: string; + paypalAccount: PayPalAccount; + coinbaseAccount: CoinbaseAccount; + applePayCard: ApplePayCard; + androidPayCard: AndroidPayCard; + visaCheckoutCard: VisaCheckoutCard; + masterpassCard: MasterpassCard; + samsungPayCard: SamsungPayCard; +} + +export interface BraintreeSubscriptionResultInterface { + transaction: BraintreeTransaction[]; + subscription: {}; // TODO add result types +} diff --git a/src/interfaces/braintree.webhook.options.interface.ts b/src/interfaces/braintree.webhook.options.interface.ts index ef66ea7..900c9b9 100644 --- a/src/interfaces/braintree.webhook.options.interface.ts +++ b/src/interfaces/braintree.webhook.options.interface.ts @@ -1,4 +1,4 @@ export interface BraintreeWebhookOptionsInterface { root: string; handle: string; -}; \ No newline at end of file +} diff --git a/src/interfaces/index.ts b/src/interfaces/index.ts index 9f6e302..ebd0959 100644 --- a/src/interfaces/index.ts +++ b/src/interfaces/index.ts @@ -2,4 +2,5 @@ export * from './braintree.options.interface'; export * from './braintree.webhook.payload.interface'; export * from './braintree.webhook.notification.interface'; export * from './braintree.webhook.methods.interface'; -export * from './braintree.webhook.options.interface'; \ No newline at end of file +export * from './braintree.webhook.options.interface'; +export * from './braintree.transaction.interface';