Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/transactions #21

Merged
merged 10 commits into from
Nov 21, 2018
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .env.dist
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
BRAINTREE_MERCHANT_ID=merchantId
BRAINTREE_PUBLIC_KEY=publicKey
BRAINTREE_PRIVATE_KEY=privateKey
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
node_modules
dist
coverage
.env
83 changes: 49 additions & 34 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<BraintreeTransactionResultInterface>`

#### Refund
`braintreeProvider.refund(transactionId: string, amount?: string, orderId?: string): Promise<BraintreeTransactionResultInterface>`

#### Find
`braintreeProvider.find(transactionId: string): Promise<BraintreeTransactionResultInterface>`

> 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
Expand Down Expand Up @@ -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 {}
```
6 changes: 3 additions & 3 deletions src/__tests__/__stubs__/config/braintree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
6 changes: 3 additions & 3 deletions src/__tests__/braintree.module.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,9 @@ describe('Braintree Module', () => {
const provider = module.get<BraintreeProvider>(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);
});

Expand Down
37 changes: 37 additions & 0 deletions src/__tests__/braintree.subscription.spec.ts
Original file line number Diff line number Diff line change
@@ -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',
// // });
// });
});
61 changes: 61 additions & 0 deletions src/__tests__/braintree.transactions.spec.ts
Original file line number Diff line number Diff line change
@@ -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);
//});
});
54 changes: 45 additions & 9 deletions src/__tests__/braintree.webhook.controller.options.spec.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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: [
Expand All @@ -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');
});
});
});
13 changes: 5 additions & 8 deletions src/__tests__/braintree.webhook.provider.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
import { Test, TestingModule } from '@nestjs/testing';
import * as path from 'path';
import { ConfigModule, ConfigService } from 'nestjs-config';
import {
BraintreeModule,
BraintreeWebhookModule,
Expand Down Expand Up @@ -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,
],
Expand Down
8 changes: 5 additions & 3 deletions src/__tests__/e2e/braintree.controller.e2e-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
],
Expand Down
44 changes: 40 additions & 4 deletions src/braintree.provider.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -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<BraintreeTransactionResultInterface> {

return await this.gateway.transaction.sale(transaction);
}

async refund(transactionId: string, amount?: string, orderId?: string): Promise<BraintreeTransactionResultInterface> {
return await this.gateway.transaction.refund(transactionId, amount, orderId);
}

async find(transactionId: string): Promise<BraintreeTransactionResultInterface> {
return await this.gateway.transaction.find(transactionId);
}

async createSubscription(subscription: BraintreeSubscriptionInterface): Promise<BraintreeSubscriptionResultInterface> {
return await this.gateway.subscription.create(subscription);
}

async cancelSubscription(subscriptionId: string): Promise<BraintreeSubscriptionResultInterface> {
return await this.gateway.subscription.cancel(subscriptionId);
}

async findSubscription(subscriptionId: string): Promise<BraintreeSubscriptionResultInterface> {
return await this.gateway.subscription.find(subscriptionId);
}

async updateSubscription(subscriptionId: string, subscription: BraintreeSubscriptionInterface): Promise<BraintreeSubscriptionResultInterface> {
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
}
Loading