-
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathsubscription.go
More file actions
353 lines (288 loc) · 11 KB
/
Copy pathsubscription.go
File metadata and controls
353 lines (288 loc) · 11 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
package gopay
import (
"context"
"errors"
"time"
)
// SubscriptionProvider extends Provider with recurring-billing capabilities:
// plans (recurring prices) and subscriptions that charge a customer's stored
// payment method each billing cycle.
//
// It is an optional interface: providers implement it only when their API
// supports subscriptions. The Client gates each method with a runtime type
// assertion and returns ErrUnsupported for providers that don't implement it.
// Subscriptions charge a saved off-session payment method, so they build on the
// setup-intent flow (see SetupIntentProvider).
type SubscriptionProvider interface {
Provider
// CreatePlan creates a recurring plan (amount + interval).
CreatePlan(ctx context.Context, req *PlanRequest) (*Plan, error)
// GetPlan retrieves a plan by ID.
GetPlan(ctx context.Context, planID string) (*Plan, error)
// CreateSubscription subscribes a customer to a plan, charging their payment
// method each billing cycle.
CreateSubscription(ctx context.Context, req *SubscriptionRequest) (*Subscription, error)
// GetSubscription retrieves a subscription by ID.
GetSubscription(ctx context.Context, subscriptionID string) (*Subscription, error)
// CancelSubscription cancels a subscription, either immediately or at the end
// of the current billing period (see CancelOptions). A nil opts cancels
// immediately.
CancelSubscription(ctx context.Context, subscriptionID string, opts *CancelOptions) (*Subscription, error)
}
// BillingInterval is the unit of a plan's recurring billing period.
type BillingInterval string
// Billing interval values.
const (
BillingIntervalDay BillingInterval = "day"
BillingIntervalWeek BillingInterval = "week"
BillingIntervalMonth BillingInterval = "month"
BillingIntervalYear BillingInterval = "year"
)
// String returns the string representation.
func (b BillingInterval) String() string { return string(b) }
// validBillingInterval reports whether b is a recognized billing interval.
func validBillingInterval(b BillingInterval) bool {
switch b {
case BillingIntervalDay, BillingIntervalWeek, BillingIntervalMonth, BillingIntervalYear:
return true
default:
return false
}
}
// PlanRequest represents a plan-creation request.
type PlanRequest struct {
// Amount is the recurring charge per interval.
Amount *Amount
// Interval is the billing interval unit (day/week/month/year).
Interval BillingInterval
// IntervalCount is the number of intervals between charges (e.g. Interval
// month + IntervalCount 3 = quarterly). Defaults to 1.
IntervalCount int
// Name is a human-readable plan name.
Name string
// Metadata holds additional data.
Metadata map[string]string
// IdempotencyKey for idempotent requests.
IdempotencyKey string
}
// NewPlanRequest creates a new plan request for the given amount and interval,
// defaulting IntervalCount to 1.
func NewPlanRequest(amount *Amount, interval BillingInterval) *PlanRequest {
return &PlanRequest{
Amount: amount,
Interval: interval,
IntervalCount: 1,
Metadata: make(map[string]string),
}
}
// WithName sets the plan name.
func (r *PlanRequest) WithName(name string) *PlanRequest {
r.Name = name
return r
}
// WithIntervalCount sets the number of intervals between charges.
func (r *PlanRequest) WithIntervalCount(count int) *PlanRequest {
r.IntervalCount = count
return r
}
// WithMetadata adds metadata.
func (r *PlanRequest) WithMetadata(key, value string) *PlanRequest {
if r.Metadata == nil {
r.Metadata = make(map[string]string)
}
r.Metadata[key] = value
return r
}
// WithIdempotencyKey sets the idempotency key.
func (r *PlanRequest) WithIdempotencyKey(key string) *PlanRequest {
r.IdempotencyKey = key
return r
}
// Validate validates the plan request.
func (r *PlanRequest) Validate() error {
if r == nil {
return errors.New("gopay: nil plan request")
}
if err := r.Amount.Validate(); err != nil {
return err
}
if !validBillingInterval(r.Interval) {
return errors.New("gopay: invalid billing interval")
}
if r.IntervalCount < 0 {
return errors.New("gopay: invalid interval count")
}
return nil
}
// Plan represents a recurring price a customer can subscribe to.
type Plan struct {
// ID is the plan ID.
ID string
// Name is the human-readable plan name.
Name string
// Amount is the recurring charge per interval.
Amount *Amount
// Interval is the billing interval unit.
Interval BillingInterval
// IntervalCount is the number of intervals between charges.
IntervalCount int
// Metadata holds additional data.
Metadata map[string]string
// CreatedAt is the creation timestamp.
CreatedAt time.Time
// Provider is the provider name.
Provider string
// Raw contains the raw provider response.
Raw map[string]any
}
// SubscriptionRequest represents a subscription-creation request.
type SubscriptionRequest struct {
// CustomerID is the customer to subscribe. Whether it is required is
// provider-dependent: providers that charge a stored payment method (Stripe)
// require it, while providers that create the customer during a
// customer-facing mandate-authorization redirect (Razorpay) ignore it. Each
// provider enforces its own requirement; core validation does not.
CustomerID string
// PlanID is the plan to subscribe the customer to.
PlanID string
// PaymentMethodID is the payment method to charge each cycle. Optional when
// the customer already has a default payment method. Providers that collect
// the mandate through a customer-facing authorization redirect (Razorpay)
// ignore it; see Subscription.AuthURL.
PaymentMethodID string
// TrialDays is the number of trial days before the first charge. Zero means
// no trial.
TrialDays int
// TotalCount is the total number of billing cycles to run before the
// subscription completes. Zero means "no fixed limit": providers that bill
// open-ended (Stripe) ignore it, while providers that require a finite cycle
// count (Razorpay) reject a zero value. Set it via WithTotalCount.
TotalCount int
// Metadata holds additional data.
Metadata map[string]string
// IdempotencyKey for idempotent requests.
IdempotencyKey string
}
// NewSubscriptionRequest creates a new subscription request for the given
// customer and plan.
func NewSubscriptionRequest(customerID, planID string) *SubscriptionRequest {
return &SubscriptionRequest{
CustomerID: customerID,
PlanID: planID,
Metadata: make(map[string]string),
}
}
// WithPaymentMethod sets the payment method to charge each cycle.
func (r *SubscriptionRequest) WithPaymentMethod(paymentMethodID string) *SubscriptionRequest {
r.PaymentMethodID = paymentMethodID
return r
}
// WithTrialDays sets the number of trial days before the first charge.
func (r *SubscriptionRequest) WithTrialDays(days int) *SubscriptionRequest {
r.TrialDays = days
return r
}
// WithTotalCount sets the total number of billing cycles before the
// subscription completes. Required by providers that only support finite
// subscriptions (Razorpay); ignored by open-ended providers (Stripe).
func (r *SubscriptionRequest) WithTotalCount(count int) *SubscriptionRequest {
r.TotalCount = count
return r
}
// WithMetadata adds metadata.
func (r *SubscriptionRequest) WithMetadata(key, value string) *SubscriptionRequest {
if r.Metadata == nil {
r.Metadata = make(map[string]string)
}
r.Metadata[key] = value
return r
}
// WithIdempotencyKey sets the idempotency key.
func (r *SubscriptionRequest) WithIdempotencyKey(key string) *SubscriptionRequest {
r.IdempotencyKey = key
return r
}
// Validate validates the subscription request.
func (r *SubscriptionRequest) Validate() error {
if r == nil {
return errors.New("gopay: nil subscription request")
}
// CustomerID is intentionally not required here: whether it is needed is
// provider-dependent (see the field doc). Providers that require it enforce
// that in their own CreateSubscription.
if r.PlanID == "" {
return errors.New("gopay: plan ID required for subscription")
}
if r.TrialDays < 0 {
return errors.New("gopay: invalid trial days")
}
if r.TotalCount < 0 {
return errors.New("gopay: invalid total count")
}
return nil
}
// CancelOptions controls how a subscription is canceled.
type CancelOptions struct {
// AtPeriodEnd cancels at the end of the current billing period instead of
// immediately. The subscription stays active (and continues to bill nothing
// further) until the period ends.
AtPeriodEnd bool
}
// SubscriptionStatus represents the status of a subscription.
type SubscriptionStatus string
// Subscription status values.
const (
SubscriptionStatusActive SubscriptionStatus = "active"
SubscriptionStatusTrialing SubscriptionStatus = "trialing"
SubscriptionStatusPastDue SubscriptionStatus = "past_due"
SubscriptionStatusCanceled SubscriptionStatus = "canceled"
SubscriptionStatusIncomplete SubscriptionStatus = "incomplete"
SubscriptionStatusIncompleteExpired SubscriptionStatus = "incomplete_expired"
SubscriptionStatusUnpaid SubscriptionStatus = "unpaid"
// SubscriptionStatusCompleted is set when a finite subscription has run all
// of its billing cycles successfully (e.g. Razorpay's "completed"). It is a
// terminal, non-failure state distinct from a cancellation.
SubscriptionStatusCompleted SubscriptionStatus = "completed"
)
// String returns the string representation.
func (s SubscriptionStatus) String() string { return string(s) }
// Subscription represents a recurring billing arrangement between a customer and
// a plan.
type Subscription struct {
// ID is the subscription ID.
ID string
// CustomerID is the subscribed customer.
CustomerID string
// PlanID is the plan being billed.
PlanID string
// PaymentMethodID is the payment method charged each cycle.
PaymentMethodID string
// Status is the subscription status.
Status SubscriptionStatus
// CurrentPeriodStart is the start of the current billing period.
CurrentPeriodStart time.Time
// CurrentPeriodEnd is the end of the current billing period (next charge).
CurrentPeriodEnd time.Time
// CancelAtPeriodEnd reports whether the subscription is set to cancel at the
// end of the current period.
CancelAtPeriodEnd bool
// CanceledAt is when the subscription was canceled; zero if not canceled.
CanceledAt time.Time
// AuthURL is a customer-facing URL the customer must visit to authorize the
// recurring mandate before billing begins. It is set only by providers whose
// subscriptions activate through a redirect/mandate flow (Razorpay); it is
// empty for providers that charge a pre-authorized payment method (Stripe).
AuthURL string
// Metadata holds additional data.
Metadata map[string]string
// CreatedAt is the creation timestamp.
CreatedAt time.Time
// Provider is the provider name.
Provider string
// Raw contains the raw provider response.
Raw map[string]any
}
// IsActive returns true if the subscription is active or in a trial period.
func (s *Subscription) IsActive() bool {
return s.Status == SubscriptionStatusActive || s.Status == SubscriptionStatusTrialing
}