Skip to content
This repository was archived by the owner on Nov 9, 2024. It is now read-only.

Commit f60762b

Browse files
committed
initial commit
1 parent abfe618 commit f60762b

37 files changed

+715
-353
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,4 @@ dist
1212
yarn.lock
1313
shrinkwrap.yaml
1414
package-lock.json
15+
tests/tmp

package.json

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"name": "adonis-shopkeeper",
2+
"name": "@foadonis/shopkeeper",
33
"description": "",
44
"version": "0.0.0",
55
"engines": {
@@ -17,11 +17,10 @@
1717
"exports": {
1818
".": "./build/index.js",
1919
"./types": "./build/src/types.js",
20-
"./fields": "./build/src/fields/main.js",
21-
"./menu": "./build/src/menu/main.js",
2220
"./providers/*": "./build/providers/*.js",
2321
"./services/*": "./build/services/*.js",
24-
"./commands": "./build/commands/main.js"
22+
"./commands": "./build/commands/main.js",
23+
"./errors": "./build/src/errors/main.js"
2524
},
2625
"scripts": {
2726
"clean": "del-cli build",
@@ -65,7 +64,7 @@
6564
"copyfiles": "^2.4.1",
6665
"del-cli": "^5.1.0",
6766
"edge.js": "^6.0.2",
68-
"eslint": "^8.57.0",
67+
"eslint": "^9.9.0",
6968
"html-entities": "^2.5.2",
7069
"np": "^10.0.6",
7170
"prettier": "^3.3.2",

providers/shopkeeper_provider.ts

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,21 +5,58 @@ import emitter from '@adonisjs/core/services/emitter'
55
import { handleCustomerSubscriptionCreated } from '../src/handlers/handle_customer_subscription_created.js'
66
import { handleCustomerSubscriptionUpdated } from '../src/handlers/handle_customer_subscription_updated.js'
77
import { handleCustomerSubscriptionDeleted } from '../src/handlers/handle_customer_subscription_deleted.js'
8+
import { handleWebhook } from '../src/handlers/handle_webhooks.js'
9+
import { InvalidConfigurationError } from '../src/errors/invalid_configuration.js'
810

911
export default class ShopkeeperProvider {
10-
constructor(protected app: ApplicationService) {}
12+
#config: Required<ShopkeeperConfig>
13+
14+
constructor(protected app: ApplicationService) {
15+
this.#config = this.app.config.get<Required<ShopkeeperConfig>>('shopkeeper')
16+
}
1117

1218
register() {
1319
this.app.container.singleton(Shopkeeper, async () => {
14-
const config = this.app.config.get<ShopkeeperConfig>('shopkeeper')
15-
return new Shopkeeper(config)
20+
const [customerModel, subscriptionModel, subscriptionItemModel] = await Promise.all([
21+
this.#config.models.customerModel().then((i) => i.default),
22+
this.#config.models.subscriptionModel().then((i) => i.default),
23+
this.#config.models.subscriptionItemModel().then((i) => i.default),
24+
])
25+
26+
return new Shopkeeper(this.#config, customerModel, subscriptionModel, subscriptionItemModel)
1627
})
1728
}
1829

19-
start() {
30+
async boot() {
31+
await this.registerRoutes()
32+
}
33+
34+
async start() {
2035
this.registerWebhookListeners()
2136
}
2237

38+
async registerRoutes() {
39+
if (this.#config.registerRoutes) {
40+
const router = await this.app.container.make('router')
41+
42+
const route = router
43+
.post('/stripe/webhook', (ctx) => handleWebhook(ctx))
44+
.as('shopkeeper.webhook')
45+
46+
if (this.#config.webhook.secret) {
47+
if (this.app.inProduction) {
48+
throw InvalidConfigurationError.webhookSecretInProduction()
49+
}
50+
51+
const middleware = router.named({
52+
stripeWebhook: () => import('../src/middlewares/stripe_webhook_middleware.js'),
53+
})
54+
55+
route.middleware(middleware.stripeWebhook())
56+
}
57+
}
58+
}
59+
2360
registerWebhookListeners() {
2461
emitter.on('stripe:customer.subscription.created', handleCustomerSubscriptionCreated)
2562
emitter.on('stripe:customer.subscription.updated', handleCustomerSubscriptionUpdated)

src/checkout.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export class Checkout {
99
/**
1010
* The Stripe model instance.
1111
*/
12+
// @ts-ignore -- Not used yet
1213
#owner: WithBillable['prototype'] | null
1314

1415
/**

src/define_config.ts

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,5 @@
11
import { ShopkeeperConfig } from './types.js'
22

3-
export function defineConfig(
4-
config: Omit<ShopkeeperConfig, 'calculateTaxes' | 'deactivatePastDue' | 'deactiveIncomplete'>
5-
): ShopkeeperConfig {
6-
return {
7-
calculateTaxes: false,
8-
deactivatePastDue: false,
9-
deactiveIncomplete: false,
10-
...config,
11-
}
3+
export function defineConfig(config: ShopkeeperConfig): ShopkeeperConfig {
4+
return config
125
}

src/errors/invalid_argument.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import { Exception } from '@adonisjs/core/exceptions'
2+
3+
export class InvalidArgumentError extends Exception {}

src/errors/invalid_configuration.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { Exception } from '@adonisjs/core/exceptions'
2+
3+
export class InvalidConfigurationError extends Exception {
4+
static webhookSecretInProduction() {
5+
return new InvalidConfigurationError(
6+
'The webhook secret is mandatory in production. Make sure the `STRIPE_WEBHOOK_SECRET` is configured.'
7+
)
8+
}
9+
}

src/errors/main.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,15 @@
11
import { CustomerAlreadyCreatedError } from './customer_already_created.js'
2+
import { IncompletePaymentError } from './incomplete_payment.js'
3+
import { InvalidArgumentError } from './invalid_argument.js'
24
import { InvalidCustomerError } from './invalid_customer.js'
5+
import { InvalidInvoiceError } from './invalid_invoice.js'
6+
import { InvalidPaymentError } from './invalid_payment.js'
7+
import { SubscriptionUpdateFailureError } from './subscription_update_failure.js'
38

49
export const E_CUSTOMER_ALREADY_CREATED = CustomerAlreadyCreatedError
10+
export const E_INCOMPLETE_PAYMENT = IncompletePaymentError
11+
export const E_INVALID_ARGUMENT = InvalidArgumentError
512
export const E_INVALID_CUSTOMER = InvalidCustomerError
13+
export const E_INVALID_INVOICE = InvalidInvoiceError
14+
export const E_INVALID_PAYMENT = InvalidPaymentError
15+
export const E_SUBSCRIPTION_UPDATE_FAILURE = SubscriptionUpdateFailureError
Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,16 @@
11
import { Exception } from '@adonisjs/core/exceptions'
2-
import { Subscription } from '../models/subscription.js'
2+
import Subscription from '../models/subscription.js'
33

4-
export class SubscriptionUpdateError extends Exception {
4+
export class SubscriptionUpdateFailureError extends Exception {
55
static incompleteSubscription(subscription: Subscription) {
6-
return new SubscriptionUpdateError(
6+
return new SubscriptionUpdateFailureError(
77
`The subscription '${subscription.stripeId}' cannot be updated because its payment is incomplete.`
88
)
99
}
10+
11+
static duplicatePrice(subscription: Subscription, price: string) {
12+
return new SubscriptionUpdateFailureError(
13+
`The price "${price}" is already attached to subscription "${subscription.stripeId}".`
14+
)
15+
}
1016
}

src/handlers/handle_customer_subscription_updated.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import Stripe from 'stripe'
22
import shopkeeper from '../../services/shopkeeper.js'
33
import { DateTime } from 'luxon'
4-
import { Subscription } from '../models/subscription.js'
54

65
export async function handleCustomerSubscriptionUpdated(
76
payload: Stripe.CustomerSubscriptionUpdatedEvent
@@ -13,7 +12,7 @@ export async function handleCustomerSubscriptionUpdated(
1312
const data = payload.data.object
1413
let subscription = await user.related('subscriptions').query().where('stripeId', data.id).first()
1514
if (!subscription) {
16-
subscription = new Subscription()
15+
subscription = new shopkeeper.subscriptionModel()
1716
subscription.stripeId = data.id
1817
}
1918

src/invoice.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import Stripe from 'stripe'
2-
import { ManagesInvoicesI, WithManagesInvoices } from './mixins/manages_invoices.js'
2+
import { WithManagesInvoices } from './mixins/manages_invoices.js'
33
import { Tax } from './tax.js'
44
import { Discount } from './discount.js'
55
import { InvalidInvoiceError } from './errors/invalid_invoice.js'
@@ -216,8 +216,8 @@ export class Invoice {
216216
return (
217217
discounts.find((amount) =>
218218
typeof amount.discount === 'string'
219-
? amount.discount === discount.id
220-
: amount.discount.id === discount.id
219+
? amount.discount === discount.asStripeDiscount().id
220+
: amount.discount.id === discount.asStripeDiscount().id
221221
)?.amount ?? null
222222
)
223223
}
@@ -550,7 +550,7 @@ export class Invoice {
550550
/**
551551
* Get the Stripe model instance.
552552
*/
553-
owner(): ManagesInvoicesI {
553+
owner(): WithManagesInvoices['prototype'] {
554554
return this.#owner
555555
}
556556

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { HttpContext } from '@adonisjs/core/http'
2+
import { NextFn } from '@adonisjs/core/types/http'
3+
import shopkeeper from '../../services/shopkeeper.js'
4+
5+
export default class StripeWebhookMiddleware {
6+
async handle({ request }: HttpContext, next: NextFn) {
7+
const sig = request.header('stripe-signature')
8+
const body = request.raw()
9+
10+
if (!body || !sig) {
11+
throw new Error('') // TODO: Error
12+
}
13+
14+
const valid = shopkeeper.stripe.webhooks.signature.verifyHeader(
15+
body,
16+
sig,
17+
shopkeeper.config.webhook.secret!, // TODO: Error
18+
shopkeeper.config.webhook.tolerance
19+
)
20+
21+
if (!valid) {
22+
throw new Error('') // TODO: Error
23+
}
24+
25+
await next()
26+
}
27+
}

src/mixins/handles_payment_failures.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import Stripe from 'stripe'
2-
import { Subscription } from '../models/subscription.js'
2+
import Subscription from '../models/subscription.js'
33
import { IncompletePaymentError } from '../errors/incomplete_payment.js'
44
import { checkStripeError } from '../utils/errors.js'
55
import { Payment } from '../payment.js'

src/mixins/handles_taxes.ts

Lines changed: 30 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -3,66 +3,44 @@ import shopkeeper from '../../services/shopkeeper.js'
33

44
type Constructor = new (...args: any[]) => {}
55

6-
export interface WithHandlesTaxes {
7-
/**
8-
* The IP address of the customer used to determine the tax location.
9-
*/
10-
customerIpAddress: string | null
11-
12-
/**
13-
* The pre-collected billing address used to estimate tax rates when performing "one-off" charges.
14-
*/
15-
estimationBillingAddress: unknown[]
16-
17-
/**
18-
* Indicates if Tax IDs should be collected during a Stripe Checkout session.
19-
*/
20-
collectTaxIds: boolean
21-
22-
/**
23-
* Set the The IP address of the customer used to determine the tax location.
24-
*/
25-
withTaxIpAddress(ipAddress: string): void
26-
27-
/**
28-
* Set a pre-collected billing address used to estimate tax rates when performing "one-off" charges.
29-
*/
30-
withTaxAddress(country: string, postalCode?: string, state?: string): void
31-
32-
/**
33-
* Get the payload for Stripe automatic tax calculation.
34-
*/
35-
automaticTaxPayload(): unknown
36-
37-
/**
38-
* Determine if automatic tax is enabled.
39-
*/
40-
isAutomaticTaxEnabled(): boolean
41-
42-
/**
43-
* Indicate that Tax IDs should be collected during a Stripe Checkout session.
44-
*/
45-
withTaxIdsCollect(): void
46-
}
47-
486
export function HandlesTaxes<Model extends Constructor>(superclass: Model) {
497
return class WithHandlesTaxes extends superclass implements WithHandlesTaxes {
8+
/**
9+
* The IP address of the customer used to determine the tax location.
10+
*/
5011
customerIpAddress: string | null = null
51-
estimationBillingAddress: unknown[] = []
12+
13+
/**
14+
* The pre-collected billing address used to estimate tax rates when performing "one-off" charges.
15+
*/
16+
estimationBillingAddress: Partial<Stripe.Address> = {}
17+
18+
/**
19+
* Indicates if Tax IDs should be collected during a Stripe Checkout session.
20+
*/
5221
collectTaxIds = false
5322

23+
/**
24+
* Set the The IP address of the customer used to determine the tax location.
25+
*/
5426
withTaxIpAddress(ipAddress: string): void {
5527
this.customerIpAddress = ipAddress
5628
}
5729

30+
/**
31+
* Set a pre-collected billing address used to estimate tax rates when performing "one-off" charges.
32+
*/
5833
withTaxAddress(country: string, postalCode?: string, state?: string): void {
5934
this.estimationBillingAddress = {
6035
country,
61-
postalCode,
36+
postal_code: postalCode,
6237
state,
6338
}
6439
}
6540

41+
/**
42+
* Get the payload for Stripe automatic tax calculation.
43+
*/
6644
automaticTaxPayload(): Stripe.SubscriptionCreateParams.AutomaticTax {
6745
return {
6846
// TODO: Check if necessary
@@ -72,12 +50,20 @@ export function HandlesTaxes<Model extends Constructor>(superclass: Model) {
7250
}
7351
}
7452

53+
/**
54+
* Determine if automatic tax is enabled.
55+
*/
7556
isAutomaticTaxEnabled(): boolean {
7657
return shopkeeper.calculateTaxes
7758
}
7859

60+
/**
61+
* Indicate that Tax IDs should be collected during a Stripe Checkout session.
62+
*/
7963
withTaxIdsCollect(): void {
8064
this.collectTaxIds = true
8165
}
8266
}
8367
}
68+
69+
export type WithHandlesTaxes = ReturnType<typeof HandlesTaxes>

src/mixins/manages_customer.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { PromotionCode } from '../promotion_code.js'
88
import { CustomerBalanceTransaction } from '../customer_balance_transaction.js'
99
import { ManagesStripeI, WithManagesStripe } from './manages_stripe.js'
1010
import { NormalizeConstructor } from '@poppinss/utils/types'
11+
import { WithBillable } from './billable.js'
1112

1213
export interface ManagesCustomerI extends ManagesStripeI<true> {
1314
/**
@@ -398,8 +399,10 @@ export function ManagesCustomer<Model extends Constructor>(superclass: Model) {
398399
...params,
399400
})
400401

402+
// TODO: Fix as unknown
401403
return transactions.data.map(
402-
(transaction) => new CustomerBalanceTransaction(this, transaction)
404+
(transaction) =>
405+
new CustomerBalanceTransaction(this as unknown as WithBillable['prototype'], transaction)
403406
)
404407
}
405408

@@ -435,7 +438,8 @@ export function ManagesCustomer<Model extends Constructor>(superclass: Model) {
435438
...params,
436439
})
437440

438-
return new CustomerBalanceTransaction(this, transaction)
441+
// TODO: Fix as any
442+
return new CustomerBalanceTransaction(this as any, transaction)
439443
}
440444

441445
preferredCurrency(): string {

0 commit comments

Comments
 (0)