Skip to content

Commit 7b4847d

Browse files
YegorZhilyamerman
andauthored
Recurring repayments (#456)
* feat: recurring repayments * fix: removed BaseCreateRepaymentRequest --------- Co-authored-by: Ilya Merman <[email protected]>
1 parent 9495382 commit 7b4847d

File tree

10 files changed

+672
-208
lines changed

10 files changed

+672
-208
lines changed

resources/recurringRepayments.ts

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import { BaseListParams, Meta, Sort, UnitConfig, UnitResponse } from "../types"
2+
import { CreateRecurringRepaymentRequest, RecurringRepayment, RecurringRepaymentStatus } from "../types/recurringRepayments"
3+
import { BaseResource } from "./baseResource"
4+
5+
export class RecurringRepayments extends BaseResource {
6+
constructor(token: string, basePath: string, config?: UnitConfig) {
7+
super(token, basePath + "/recurring-repayments", config)
8+
}
9+
10+
public async create(request: CreateRecurringRepaymentRequest): Promise<UnitResponse<RecurringRepayment>> {
11+
return this.httpPost<UnitResponse<RecurringRepayment>>("", { data: request })
12+
}
13+
14+
public async get(repaymentId: string): Promise<UnitResponse<RecurringRepayment>> {
15+
return this.httpGet<UnitResponse<RecurringRepayment>>(`/${repaymentId}`)
16+
}
17+
18+
public async disable(repaymentId: string): Promise<UnitResponse<RecurringRepayment>> {
19+
// NB: We must pass an empty body here because the API is returning a 415 for any requests made
20+
// to this endpoint from this SDK: Content-Type must be application/vnd.api+json
21+
// However, there is code in Axios that strips the Content-Type from the request whenever
22+
// there is no body since it makes no sense to provide it + makes the data over the wire
23+
// that much smaller:
24+
// https://github.com/axios/axios/blob/649d739288c8e2c55829ac60e2345a0f3439c730/dist/axios.js#L1449-L1450
25+
// TODO: Remove the empty body after we update the API to not throw 415 for missing Content-Type
26+
// when the body is empty.
27+
return this.httpPost<UnitResponse<RecurringRepayment>>(`/${repaymentId}/disable`, { data: {} })
28+
}
29+
30+
public async enable(repaymentId: string): Promise<UnitResponse<RecurringRepayment>> {
31+
// NB: We must pass an empty body here because the API is returning a 415 for any requests made
32+
// to this endpoint from this SDK: Content-Type must be application/vnd.api+json
33+
// However, there is code in Axios that strips the Content-Type from the request whenever
34+
// there is no body since it makes no sense to provide it + makes the data over the wire
35+
// that much smaller:
36+
// https://github.com/axios/axios/blob/649d739288c8e2c55829ac60e2345a0f3439c730/dist/axios.js#L1449-L1450
37+
// TODO: Remove the empty body after we update the API to not throw 415 for missing Content-Type
38+
// when the body is empty.
39+
return this.httpPost<UnitResponse<RecurringRepayment>>(`/${repaymentId}/enable`, { data: {} })
40+
}
41+
42+
public async list(params?: RecurringPaymentListParams): Promise<UnitResponse<RecurringRepayment[]>> {
43+
const parameters: any = {
44+
"page[limit]": (params?.limit ? params.limit : 100),
45+
"page[offset]": (params?.offset ? params.offset : 0),
46+
...(params?.creditAccountId && { "filter[creditAccountId]": params.creditAccountId }),
47+
...(params?.customerId && { "filter[customerId]": params.customerId }),
48+
...(params?.status && { "filter[status]": params.status }),
49+
...(params?.fromStartTime && { "filter[fromStartTime]": params.fromStartTime }),
50+
...(params?.toStartTime && { "filter[toStartTime]": params.toStartTime }),
51+
"sort": params?.sort ? params.sort : "-createdAt"
52+
}
53+
54+
if (params?.status)
55+
params.status.forEach((s, idx) => {
56+
parameters[`filter[status][${idx}]`] = s
57+
})
58+
59+
return this.httpGet<UnitResponse<RecurringRepayment[] & Meta>>("", { params: parameters })
60+
}
61+
}
62+
63+
export interface RecurringPaymentListParams extends BaseListParams {
64+
/**
65+
* Optional. Filters the results by the specified credit account id.
66+
* default: empty
67+
*/
68+
creditAccountId?: string
69+
70+
/**
71+
* Optional. Filters the results by the specified customer id.
72+
* default: empty
73+
*/
74+
customerId?: string
75+
76+
/**
77+
* Optional. Filter recurring payments by status (Active or Disabled). Usage example: *filter[status][0]=Active
78+
*/
79+
status?: RecurringRepaymentStatus[]
80+
81+
/**
82+
* RFC3339 format. For more information: https://en.wikipedia.org/wiki/ISO_8601#RFCs
83+
* Optional. Filters the Recurring Payments that their start time occurred after the specified date. e.g. 2022-06-13
84+
*/
85+
fromStartTime?: string
86+
87+
/**
88+
* RFC3339 format. For more information: https://en.wikipedia.org/wiki/ISO_8601#RFCs
89+
* Optional. Filters the Recurring Payments that their start time occurred before the specified date. e.g. 2022-05-13
90+
*/
91+
toStartTime?: string
92+
93+
/**
94+
* Optional. Leave empty or provide sort = createdAt for ascending order.Provide sort = -createdAt(leading minus sign) for descending order.
95+
* default: sort=-createdAt
96+
*/
97+
sort?: Sort
98+
}

tests/recurringRepayments.spec.ts

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
import { CreateRecurringAchRepaymentRequest, CreateRecurringBookRepaymentRequest, CreateRecurringCapitalPartnerAchRepaymentRequest, CreateRecurringCapitalPartnerBookRepaymentRequest } from "../types/recurringRepayments"
2+
import { Unit } from "../unit"
3+
import { initRepaymentRelatedRelationships } from "./testHelpers"
4+
import dotenv from "dotenv"
5+
6+
dotenv.config()
7+
const unit = new Unit(process.env.UNIT_TOKEN || "test", process.env.UNIT_API_URL || "test")
8+
const repaymentsId: string[] = []
9+
10+
describe("Create RecurringAchRepayment", () => {
11+
test("create recurringachrepayment", async () => {
12+
const data = await initRepaymentRelatedRelationships(unit)
13+
const req: CreateRecurringAchRepaymentRequest = {
14+
"type": "recurringAchRepayment",
15+
"attributes": {
16+
"description": "Rent"
17+
},
18+
"relationships": {
19+
"account": {
20+
"data": {
21+
"type": "depositAccount",
22+
"id": data.depositAccountId
23+
}
24+
},
25+
"counterparty": {
26+
"data": {
27+
"type": "counterparty",
28+
"id": data.plaidCounterpartyId
29+
}
30+
},
31+
creditAccount: {
32+
data: {
33+
type: "creditAccount",
34+
id: data.nonPartnerCreditAccountId
35+
}
36+
}
37+
}
38+
}
39+
40+
const res = await unit.recurringRepayments.create(req)
41+
expect(res.data.type === "recurringAchRepayment").toBeTruthy()
42+
}, 180000)
43+
})
44+
45+
describe("Create RecurringBookRepayment", () => {
46+
test("create recurringbookrepayment", async () => {
47+
const data = await initRepaymentRelatedRelationships(unit)
48+
const req: CreateRecurringBookRepaymentRequest = {
49+
"type": "recurringBookRepayment",
50+
"attributes": {
51+
"description": "Rent"
52+
},
53+
"relationships": {
54+
"account": {
55+
"data": {
56+
"type": "depositAccount",
57+
"id": data.depositAccountId
58+
}
59+
},
60+
"counterpartyAccount": {
61+
"data": {
62+
"type": "depositAccount",
63+
"id": data.anotherDepositAccountId
64+
}
65+
},
66+
creditAccount: {
67+
data: {
68+
type: "creditAccount",
69+
id: data.nonPartnerCreditAccountId
70+
}
71+
}
72+
}
73+
}
74+
75+
const res = await unit.recurringRepayments.create(req)
76+
expect(res.data.type === "recurringBookRepayment").toBeTruthy()
77+
}, 180000)
78+
})
79+
80+
describe("Create CapitalPartnerRecurringAchRepayment", () => {
81+
test("create recurringacharepayment", async () => {
82+
const data = await initRepaymentRelatedRelationships(unit)
83+
const req: CreateRecurringCapitalPartnerAchRepaymentRequest = {
84+
"type": "recurringCapitalPartnerAchRepayment",
85+
"attributes": {
86+
"description": "Rent",
87+
"addenda": "Test addenda"
88+
},
89+
"relationships": {
90+
"counterparty": {
91+
"data": {
92+
"type": "counterparty",
93+
"id": data.plaidCounterpartyId
94+
}
95+
},
96+
creditAccount: {
97+
data: {
98+
type: "creditAccount",
99+
id: data.partnerCreditAccountId
100+
}
101+
}
102+
}
103+
}
104+
105+
const res = await unit.recurringRepayments.create(req)
106+
expect(res.data.type === "recurringCapitalPartnerAchRepayment").toBeTruthy()
107+
}, 180000)
108+
})
109+
110+
describe("Create CapitalPartnerRecurringBookRepayment", () => {
111+
test("create capitalpartnerbookrepayment", async () => {
112+
const data = await initRepaymentRelatedRelationships(unit)
113+
const req: CreateRecurringCapitalPartnerBookRepaymentRequest =
114+
{
115+
"type": "recurringCapitalPartnerBookRepayment",
116+
"attributes": {
117+
"description": "Rent",
118+
},
119+
"relationships": {
120+
"counterpartyAccount": {
121+
"data": {
122+
"type": "depositAccount",
123+
"id": data.anotherDepositAccountId
124+
}
125+
},
126+
creditAccount: {
127+
data: {
128+
type: "creditAccount",
129+
id: data.partnerCreditAccountId
130+
}
131+
}
132+
}
133+
}
134+
135+
const res = await unit.recurringRepayments.create(req)
136+
expect(res.data.type === "recurringCapitalPartnerBookRepayment").toBeTruthy()
137+
}, 180000)
138+
})
139+
140+
describe("Repayments List", () => {
141+
test("get recurring repayments List", async () => {
142+
const res = await unit.recurringRepayments.list()
143+
res.data.forEach(element => {
144+
expect(element.type).toContain("Repayment")
145+
repaymentsId.push(element.id)
146+
})
147+
})
148+
})
149+
150+
describe("Get Repayment Test", () => {
151+
test("get each recurring repayment", async () => {
152+
const repayments = (await unit.repayments.list()).data
153+
repayments.forEach(async rp => {
154+
const res = await unit.repayments.get(rp.id)
155+
expect(res.data.type).toContain("Repayment")
156+
})
157+
})
158+
})
159+
160+
describe("Disable And Enable Recurring Repayment", () => {
161+
test("disable recurring repayment", async () => {
162+
const res = await unit.recurringRepayments.disable(repaymentsId[0])
163+
expect(res.data.attributes.status).toBe("Disabled")
164+
})
165+
166+
test("enable recurring repayment", async () => {
167+
const res = await unit.recurringRepayments.enable(repaymentsId[0])
168+
expect(res.data.attributes.status).toBe("Active")
169+
})
170+
})

tests/repayments.spec.ts

Lines changed: 9 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import { CreateAchRepaymentRequest, CreateBookRepaymentRequest, CreateBusinessCreditCardRequest, CreateCapitalPartnerAchRepaymentRequest, CreateCapitalPartnerBookRepayment, CreateCardPurchaseSimulation, Unit } from "../unit"
1+
import { CreateAchRepaymentRequest, CreateBookRepaymentRequest, CreateCapitalPartnerAchRepaymentRequest, CreateCapitalPartnerBookRepayment, Unit } from "../unit"
22

33
import dotenv from "dotenv"
4-
import { createCreditAccount, createIndividualAccount, createPlaidCounterparty } from "./testHelpers"
4+
import { initRepaymentRelatedRelationships } from "./testHelpers"
55

66
dotenv.config()
77
const unit = new Unit(process.env.UNIT_TOKEN || "test", process.env.UNIT_API_URL || "test")
@@ -12,85 +12,14 @@ let depositAccountId = ""
1212
let anotherDepositAccountId = ""
1313
let plaidCounterpartyId = ""
1414

15-
const simulateCardPurchase = async (creditAccountId: string) => {
16-
const createCardReq: CreateBusinessCreditCardRequest = {
17-
type: "businessCreditCard",
18-
attributes: {
19-
shippingAddress: {
20-
street: "5230 Newell Rd",
21-
city: "Palo Alto",
22-
state: "CA",
23-
postalCode: "94303",
24-
country: "US",
25-
},
26-
fullName: {
27-
first: "Richard",
28-
last: "Hendricks",
29-
},
30-
address: {
31-
street: "5230 Newell Rd",
32-
city: "Palo Alto",
33-
state: "CA",
34-
postalCode: "94303",
35-
country: "US",
36-
},
37-
dateOfBirth: "2001-08-10",
38-
39-
phone: {
40-
countryCode: "1",
41-
number: "5555555555",
42-
},
43-
},
44-
relationships: {
45-
account: {
46-
data: {
47-
type: "creditAccount",
48-
id: creditAccountId,
49-
},
50-
},
51-
},
52-
}
53-
54-
55-
const createCreditCardResponse = await unit.cards.create(createCardReq)
56-
await unit.simulations.activateCard(createCreditCardResponse.data.id)
57-
58-
const purchaseReq: CreateCardPurchaseSimulation = {
59-
type: "purchaseTransaction",
60-
attributes: {
61-
amount: 2000,
62-
direction: "Credit",
63-
merchantName: "Apple Inc.",
64-
merchantType: 1000,
65-
merchantLocation: "Cupertino, CA",
66-
last4Digits: createCreditCardResponse.data.attributes.last4Digits,
67-
internationalServiceFee: 50,
68-
},
69-
relationships: {
70-
account: {
71-
data: {
72-
type: "creditAccount",
73-
id: creditAccountId,
74-
},
75-
},
76-
},
77-
}
78-
79-
await unit.simulations.createCardPurchase(purchaseReq)
80-
}
81-
82-
describe("Init repayments related resources", () => {
15+
describe("Init Repayments Related Resources", () => {
8316
test("init resources", async () => {
84-
const creditAccountRes = await createCreditAccount(unit)
85-
const depositAccountRes = (await createIndividualAccount(unit))
86-
partnerCreditAccountId = creditAccountRes.data.id
87-
nonPartnerCreditAccountId = (await createCreditAccount(unit, "credit_terms_choice")).data.id
88-
depositAccountId = depositAccountRes.data.id
89-
anotherDepositAccountId = (await createIndividualAccount(unit)).data.id
90-
plaidCounterpartyId = (await createPlaidCounterparty(unit))
91-
92-
await simulateCardPurchase(partnerCreditAccountId)
93-
await simulateCardPurchase(nonPartnerCreditAccountId)
17+
const res = await initRepaymentRelatedRelationships(unit)
18+
partnerCreditAccountId = res.partnerCreditAccountId
19+
nonPartnerCreditAccountId = res.nonPartnerCreditAccountId
20+
depositAccountId = res.depositAccountId
21+
anotherDepositAccountId = res.anotherDepositAccountId
22+
plaidCounterpartyId = res.plaidCounterpartyId
9423

9524
}, 180000)
9625
})

0 commit comments

Comments
 (0)