Skip to content

Commit 5eff83d

Browse files
Piyush Singh GaurPiyush Singh Gaur
authored andcommitted
fix(provider): accept payment behavior from param
accept payment behavior from param GH-21
1 parent 7656781 commit 5eff83d

6 files changed

Lines changed: 71 additions & 24 deletions

File tree

src/__tests__/unit/stripe-subscription.service.unit.ts

Lines changed: 41 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import {expect, sinon} from '@loopback/testlab';
22
import {StripeService} from '../../providers/sdk/stripe/stripe.service';
33
import {
44
CollectionMethod,
5+
PaymentBehavior,
56
ProrationBehavior,
67
RecurringInterval,
78
TPrice,
@@ -253,19 +254,52 @@ describe('StripeService - Subscription Management', () => {
253254
expect(callArg.collection_method).to.equal(CollectionMethod.SEND_INVOICE);
254255
});
255256

256-
it('uses defaultPaymentBehavior from StripeConfig when provided', async () => {
257-
// Create a separate service instance with a custom payment behavior so
258-
// callers are not forced to use the SCA-default 'default_incomplete'.
257+
it('uses per-call paymentBehavior when provided on TSubscriptionCreate', async () => {
258+
stripeStub.subscriptions.create.resolves({id: 'sub_custom_003'});
259+
260+
await service.createSubscription({
261+
customerId: 'cus_custom',
262+
priceRefId: 'price_abc',
263+
collectionMethod: CollectionMethod.CHARGE_AUTOMATICALLY,
264+
paymentBehavior: PaymentBehavior.ALLOW_INCOMPLETE,
265+
});
266+
267+
const callArg = stripeStub.subscriptions.create.firstCall.args[0];
268+
expect(callArg.payment_behavior).to.equal('allow_incomplete');
269+
});
270+
271+
it('per-call paymentBehavior takes priority over StripeConfig.defaultPaymentBehavior', async () => {
272+
// Config says 'error_if_incomplete', but the per-call param overrides it.
273+
const customService = new StripeService({
274+
secretKey: 'sk_test_dummy',
275+
defaultPaymentBehavior: 'error_if_incomplete',
276+
});
277+
(customService as unknown as {stripe: StubbedStripe}).stripe = stripeStub;
278+
279+
stripeStub.subscriptions.create.resolves({id: 'sub_override_004'});
280+
281+
await customService.createSubscription({
282+
customerId: 'cus_override',
283+
priceRefId: 'price_abc',
284+
collectionMethod: CollectionMethod.CHARGE_AUTOMATICALLY,
285+
paymentBehavior: PaymentBehavior.ALLOW_INCOMPLETE,
286+
});
287+
288+
const callArg = stripeStub.subscriptions.create.firstCall.args[0];
289+
expect(callArg.payment_behavior).to.equal('allow_incomplete');
290+
});
291+
292+
it('falls back to StripeConfig.defaultPaymentBehavior when no per-call value given', async () => {
259293
const customService = new StripeService({
260294
secretKey: 'sk_test_dummy',
261295
defaultPaymentBehavior: 'allow_incomplete',
262296
});
263297
(customService as unknown as {stripe: StubbedStripe}).stripe = stripeStub;
264298

265-
stripeStub.subscriptions.create.resolves({id: 'sub_custom_003'});
299+
stripeStub.subscriptions.create.resolves({id: 'sub_config_005'});
266300

267301
await customService.createSubscription({
268-
customerId: 'cus_custom',
302+
customerId: 'cus_config',
269303
priceRefId: 'price_abc',
270304
collectionMethod: CollectionMethod.CHARGE_AUTOMATICALLY,
271305
});
@@ -274,8 +308,8 @@ describe('StripeService - Subscription Management', () => {
274308
expect(callArg.payment_behavior).to.equal('allow_incomplete');
275309
});
276310

277-
it('falls back to default_incomplete when defaultPaymentBehavior is not configured', async () => {
278-
stripeStub.subscriptions.create.resolves({id: 'sub_default_004'});
311+
it('falls back to default_incomplete when neither per-call nor config is provided', async () => {
312+
stripeStub.subscriptions.create.resolves({id: 'sub_default_006'});
279313

280314
await service.createSubscription({
281315
customerId: 'cus_fallback',

src/keys.ts

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import {BindingKey, CoreBindings} from '@loopback/core';
22
import {BillingComponent} from './component';
3-
import {IService, ISubscriptionService} from './types';
3+
import {IService} from './types';
44

55
/**
66
* Binding keys used by this component.
@@ -12,13 +12,4 @@ export namespace BillingComponentBindings {
1212
export const BillingProvider = BindingKey.create<IService>('sf.billing');
1313
export const SDKProvider = BindingKey.create<IService>('sf.billing.sdk');
1414
export const RestProvider = BindingKey.create<IService>('sf.billing.rest');
15-
/**
16-
* Binding key for a provider that implements the full subscription lifecycle
17-
* ({@link ISubscriptionService}). Bind your extended StripeService (or any
18-
* other gateway implementation) here so controllers and services can inject
19-
* subscription capabilities independently of one-time billing.
20-
*/
21-
export const SubscriptionProvider = BindingKey.create<ISubscriptionService>(
22-
'sf.billing.subscription',
23-
);
2415
}

src/providers/sdk/stripe/stripe.service.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -332,7 +332,8 @@ export class StripeService implements IStripeService {
332332
const params = this.stripeSubscriptionAdapter.adaptFromModel(subscription);
333333
const created = await this.stripe.subscriptions.create({
334334
...params,
335-
payment_behavior: (this.stripeConfig.defaultPaymentBehavior ??
335+
payment_behavior: (subscription.paymentBehavior ??
336+
this.stripeConfig.defaultPaymentBehavior ??
336337
'default_incomplete') as Stripe.SubscriptionCreateParams.PaymentBehavior,
337338
});
338339
return created.id;

src/providers/sdk/stripe/type/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import {IService, ISubscriptionService} from '../../../../types';
55
* and recurring-subscription management ({@link ISubscriptionService}).
66
*
77
* Implementors can bind to {@link BillingComponentBindings.SDKProvider} for
8-
* one-time billing OR to {@link BillingComponentBindings.SubscriptionProvider}
8+
* one-time billing OR to {@link BillingComponentBindings.SDKProvider}
99
* for subscription operations, depending on their needs (ISP).
1010
*/
1111
export interface IStripeService extends IService, ISubscriptionService {}

src/providers/sdk/stripe/type/stripe-config.type.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,13 @@ import Stripe from 'stripe';
22
export interface StripeConfig {
33
secretKey: string;
44
/**
5-
* Controls how Stripe handles payment during subscription creation.
6-
* Defaults to `'default_incomplete'` (SCA-compliant: subscription starts
7-
* incomplete until the first payment is confirmed).
5+
* Global fallback for Stripe payment behaviour during subscription creation.
6+
* Defaults to `'default_incomplete'` when neither this nor a per-call
7+
* `TSubscriptionCreate.paymentBehavior` is provided.
88
*
9-
* Set to `'allow_incomplete'` or `'error_if_incomplete'` to change the
10-
* behaviour for your integration.
9+
* Per-call `paymentBehavior` on `TSubscriptionCreate` takes highest priority,
10+
* so different subscriptions can use different behaviours without changing
11+
* this global setting.
1112
*
1213
* @see https://stripe.com/docs/api/subscriptions/create#create_subscription-payment_behavior
1314
*/

src/types.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,20 @@ export enum RecurringInterval {
141141
YEAR = 'year',
142142
}
143143

144+
/**
145+
* Controls how Stripe handles payment collection during subscription creation.
146+
* Pass this per-call on {@link TSubscriptionCreate.paymentBehavior}; falls back
147+
* to `StripeConfig.defaultPaymentBehavior`, then `'default_incomplete'`.
148+
*
149+
* @see https://stripe.com/docs/api/subscriptions/create#create_subscription-payment_behavior
150+
*/
151+
export enum PaymentBehavior {
152+
DEFAULT_INCOMPLETE = 'default_incomplete',
153+
ALLOW_INCOMPLETE = 'allow_incomplete',
154+
ERROR_IF_INCOMPLETE = 'error_if_incomplete',
155+
PENDING_IF_INCOMPLETE = 'pending_if_incomplete',
156+
}
157+
144158
/**
145159
* Controls how prorations are calculated when a subscription is updated.
146160
*/
@@ -186,6 +200,12 @@ export interface TSubscriptionCreate {
186200
collectionMethod: CollectionMethod;
187201
/** Number of days after which the invoice is due (applicable for send_invoice). */
188202
daysUntilDue?: number;
203+
/**
204+
* Stripe-specific: controls payment behaviour during subscription creation.
205+
* Takes highest priority over `StripeConfig.defaultPaymentBehavior`.
206+
* Ignored by non-Stripe providers.
207+
*/
208+
paymentBehavior?: PaymentBehavior;
189209
}
190210

191211
/**

0 commit comments

Comments
 (0)