09ec5f107d
During billing, before invoice creation, check if users are part of a package plan. If so, and if the package plan is expired, remove unused credit from the user's balance. If the user has credit in addition to the package credit, send an analytics event to notify someone to handle the credit removal manually. Change-Id: Iad71d791f67c9733f9d9e42f962c64b2780264cc
876 lines
23 KiB
Go
876 lines
23 KiB
Go
// Copyright (C) 2020 Storj Labs, Inc.
|
|
// See LICENSE for copying information.
|
|
|
|
package stripe
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/stripe/stripe-go/v72"
|
|
"github.com/stripe/stripe-go/v72/charge"
|
|
"github.com/stripe/stripe-go/v72/customer"
|
|
"github.com/stripe/stripe-go/v72/customerbalancetransaction"
|
|
"github.com/stripe/stripe-go/v72/form"
|
|
"github.com/stripe/stripe-go/v72/invoice"
|
|
"github.com/stripe/stripe-go/v72/invoiceitem"
|
|
"github.com/stripe/stripe-go/v72/paymentmethod"
|
|
"github.com/stripe/stripe-go/v72/promotioncode"
|
|
|
|
"storj.io/common/testrand"
|
|
"storj.io/common/uuid"
|
|
"storj.io/storj/satellite/console"
|
|
)
|
|
|
|
const (
|
|
// MockCouponID1 is a coupon that stripe mock is aware of. Applying unknown coupons results in failure.
|
|
MockCouponID1 = "c1"
|
|
// MockCouponID2 is a coupon that stripe mock is aware of. Applying unknown coupons results in failure.
|
|
MockCouponID2 = "c2"
|
|
// MockCouponID3 is a coupon that stripe mock is aware of. Applying unknown coupons results in failure.
|
|
MockCouponID3 = "c3"
|
|
|
|
// MockInvoicesNewFailure can be passed to mockInvoices.New as `desc` argument to cause it to return
|
|
// an error.
|
|
MockInvoicesNewFailure = "mock_invoices_new_failure"
|
|
|
|
// MockInvoicesPayFailure can be passed to mockInvoices.Pay as params.PaymentMethod to cause it to return
|
|
// an error.
|
|
MockInvoicesPayFailure = "mock_invoices_pay_failure"
|
|
|
|
// MockInvoicesPaySuccess can be passed to mockInvoices.Pay as params.PaymentMethod to cause it to return
|
|
// a paid invoice.
|
|
MockInvoicesPaySuccess = "mock_invoices_pay_success"
|
|
|
|
// TestPaymentMethodsNewFailure can be passed to creditCards.Add as the cardToken arg to cause
|
|
// mockPaymentMethods.New to return an error.
|
|
TestPaymentMethodsNewFailure = "test_payment_methods_new_failure"
|
|
|
|
// TestPaymentMethodsAttachFailure can be passed to creditCards.Add as the cardToken arg to cause
|
|
// mockPaymentMethods.Attach to return an error.
|
|
TestPaymentMethodsAttachFailure = "test_payment_methods_attach_failure"
|
|
|
|
// MockCBTXsNewFailure can be passed to mockCustomerBalanceTransactions.New as the `desc` argument to cause it
|
|
// to return an error.
|
|
MockCBTXsNewFailure = "mock_cbtxs_new_failure"
|
|
)
|
|
|
|
var (
|
|
testPromoCodes = map[string]*stripe.PromotionCode{
|
|
"promo1": {
|
|
ID: "p1",
|
|
Coupon: &stripe.Coupon{
|
|
AmountOff: 500,
|
|
Currency: stripe.CurrencyUSD,
|
|
Name: "Test Promo Code 1",
|
|
ID: MockCouponID1,
|
|
},
|
|
},
|
|
"promo2": {
|
|
ID: "p2",
|
|
Coupon: &stripe.Coupon{
|
|
PercentOff: 50,
|
|
Name: "Test Promo Code 2",
|
|
ID: MockCouponID2,
|
|
},
|
|
},
|
|
"promo3": {
|
|
ID: "p3",
|
|
Coupon: &stripe.Coupon{
|
|
AmountOff: 100,
|
|
Currency: stripe.CurrencyUSD,
|
|
Name: "Test Promo Code 3",
|
|
ID: MockCouponID3,
|
|
},
|
|
},
|
|
}
|
|
promoIDs = map[string]*stripe.PromotionCode{
|
|
"p1": testPromoCodes["promo1"],
|
|
"p2": testPromoCodes["promo2"],
|
|
}
|
|
mockCoupons = map[string]*stripe.Coupon{
|
|
MockCouponID1: testPromoCodes["promo1"].Coupon,
|
|
MockCouponID2: testPromoCodes["promo2"].Coupon,
|
|
MockCouponID3: testPromoCodes["promo3"].Coupon,
|
|
}
|
|
)
|
|
|
|
// mockStripeState Stripe client mock.
|
|
type mockStripeState struct {
|
|
mu sync.Mutex
|
|
|
|
customers *mockCustomersState
|
|
paymentMethods *mockPaymentMethods
|
|
invoices *mockInvoices
|
|
invoiceItems *mockInvoiceItems
|
|
customerBalanceTransactions *mockCustomerBalanceTransactions
|
|
charges *mockCharges
|
|
promoCodes *mockPromoCodes
|
|
creditNotes *mockCreditNotes
|
|
}
|
|
|
|
type mockStripeClient struct {
|
|
customersDB CustomersDB
|
|
usersDB console.Users
|
|
*mockStripeState
|
|
}
|
|
|
|
// mockEmptyQuery is a query with no results.
|
|
var mockEmptyQuery = stripe.Query(func(*stripe.Params, *form.Values) ([]interface{}, stripe.ListContainer, error) {
|
|
return nil, newListContainer(&stripe.ListMeta{}), nil
|
|
})
|
|
|
|
// NewStripeMock creates new Stripe client mock.
|
|
//
|
|
// A new mock is returned for each unique id. If this method is called multiple
|
|
// times with the same id, it will return the same mock instance for that id.
|
|
//
|
|
// If called by satellite component, the id param should be the peer.ID().
|
|
// If called by CLI tool, the id param should be a zero value, i.e. storj.NodeID{}.
|
|
// If called by satellitedb test case, the id param should be a random value,
|
|
// i.e. testrand.NodeID().
|
|
func NewStripeMock(customersDB CustomersDB, usersDB console.Users) Client {
|
|
state := &mockStripeState{}
|
|
state.customers = &mockCustomersState{}
|
|
state.paymentMethods = newMockPaymentMethods(state)
|
|
state.invoiceItems = newMockInvoiceItems(state)
|
|
state.invoices = newMockInvoices(state, state.invoiceItems)
|
|
state.customerBalanceTransactions = newMockCustomerBalanceTransactions(state)
|
|
state.charges = &mockCharges{}
|
|
state.promoCodes = newMockPromoCodes(state)
|
|
state.creditNotes = newMockCreditNotes(state)
|
|
|
|
return &mockStripeClient{
|
|
customersDB: customersDB,
|
|
usersDB: usersDB,
|
|
mockStripeState: state,
|
|
}
|
|
}
|
|
|
|
func (m *mockStripeClient) Customers() Customers {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
return &mockCustomers{
|
|
root: m.mockStripeState,
|
|
|
|
customersDB: m.customersDB,
|
|
usersDB: m.usersDB,
|
|
state: m.customers,
|
|
coupons: mockCoupons,
|
|
}
|
|
}
|
|
|
|
func (m *mockStripeClient) PaymentMethods() PaymentMethods {
|
|
return m.paymentMethods
|
|
}
|
|
|
|
func (m *mockStripeClient) Invoices() Invoices {
|
|
return m.invoices
|
|
}
|
|
|
|
func (m *mockStripeClient) InvoiceItems() InvoiceItems {
|
|
return m.invoiceItems
|
|
}
|
|
|
|
func (m *mockStripeClient) CustomerBalanceTransactions() CustomerBalanceTransactions {
|
|
return m.customerBalanceTransactions
|
|
}
|
|
|
|
func (m *mockStripeClient) Charges() Charges {
|
|
return m.charges
|
|
}
|
|
|
|
func (m *mockStripeClient) PromoCodes() PromoCodes {
|
|
return m.promoCodes
|
|
}
|
|
|
|
func (m *mockStripeClient) CreditNotes() CreditNotes {
|
|
return m.creditNotes
|
|
}
|
|
|
|
type mockCustomers struct {
|
|
root *mockStripeState
|
|
|
|
customersDB CustomersDB
|
|
usersDB console.Users
|
|
state *mockCustomersState
|
|
coupons map[string]*stripe.Coupon
|
|
}
|
|
|
|
func (m *mockCustomers) List(listParams *stripe.CustomerListParams) *customer.Iter {
|
|
m.root.mu.Lock()
|
|
defer m.root.mu.Unlock()
|
|
|
|
return &customer.Iter{
|
|
Iter: stripe.GetIter(listParams, func(p *stripe.Params, vals *form.Values) ([]interface{}, stripe.ListContainer, error) {
|
|
var customers []interface{}
|
|
for _, cus := range m.state.customers {
|
|
customers = append(customers, cus)
|
|
}
|
|
return customers, newListContainer(&stripe.ListMeta{}), nil
|
|
}),
|
|
}
|
|
}
|
|
|
|
type mockCustomersState struct {
|
|
customers []*stripe.Customer
|
|
repopulated bool
|
|
}
|
|
|
|
// The Stripe Client Mock is in-memory so all data is lost when the satellite is stopped.
|
|
// We need to repopulate the mock on every restart to ensure that requests to the mock
|
|
// for existing users won't fail with errors like "customer not found".
|
|
func (m *mockCustomers) repopulate() error {
|
|
m.root.mu.Lock()
|
|
defer m.root.mu.Unlock()
|
|
|
|
if !m.state.repopulated {
|
|
const limit = 25
|
|
ctx := context.TODO()
|
|
|
|
cusPage, err := m.customersDB.List(ctx, uuid.UUID{}, limit, time.Now())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, cus := range cusPage.Customers {
|
|
user, err := m.usersDB.Get(ctx, cus.UserID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
m.state.customers = append(m.state.customers, newMockCustomer(cus.ID, user.Email))
|
|
}
|
|
|
|
for cusPage.Next {
|
|
cusPage, err = m.customersDB.List(ctx, cusPage.Cursor, limit, time.Now())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, cus := range cusPage.Customers {
|
|
user, err := m.usersDB.Get(ctx, cus.UserID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
m.state.customers = append(m.state.customers, newMockCustomer(cus.ID, user.Email))
|
|
}
|
|
}
|
|
|
|
m.state.repopulated = true
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func newMockCustomer(id, email string) *stripe.Customer {
|
|
return &stripe.Customer{
|
|
ID: id,
|
|
Email: email,
|
|
InvoiceSettings: &stripe.CustomerInvoiceSettings{
|
|
DefaultPaymentMethod: &stripe.PaymentMethod{
|
|
ID: "pm_card_mastercard",
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
func (m *mockCustomers) New(params *stripe.CustomerParams) (*stripe.Customer, error) {
|
|
if err := m.repopulate(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
uuid, err := uuid.New()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
customer := newMockCustomer(uuid.String(), *params.Email)
|
|
|
|
if params.PromotionCode != nil && promoIDs[*params.PromotionCode] != nil {
|
|
customer.Discount = &stripe.Discount{Coupon: promoIDs[*params.PromotionCode].Coupon}
|
|
}
|
|
|
|
if params.Coupon != nil && *params.Coupon != "" {
|
|
c, ok := m.coupons[*params.Coupon]
|
|
if !ok {
|
|
return nil, &stripe.Error{}
|
|
}
|
|
customer.Discount = &stripe.Discount{Coupon: mockCoupons[c.ID]}
|
|
}
|
|
|
|
m.root.mu.Lock()
|
|
defer m.root.mu.Unlock()
|
|
|
|
m.state.customers = append(m.state.customers, customer)
|
|
return customer, nil
|
|
}
|
|
|
|
func (m *mockCustomers) Get(id string, params *stripe.CustomerParams) (*stripe.Customer, error) {
|
|
if err := m.repopulate(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
m.root.mu.Lock()
|
|
defer m.root.mu.Unlock()
|
|
|
|
for _, customer := range m.state.customers {
|
|
if id == customer.ID {
|
|
return customer, nil
|
|
}
|
|
}
|
|
|
|
return nil, errors.New("customer not found")
|
|
}
|
|
|
|
func (m *mockCustomers) Update(id string, params *stripe.CustomerParams) (*stripe.Customer, error) {
|
|
if err := m.repopulate(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
customer, err := m.Get(id, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if params == nil {
|
|
return customer, nil
|
|
}
|
|
|
|
m.root.mu.Lock()
|
|
defer m.root.mu.Unlock()
|
|
|
|
if params.Metadata != nil {
|
|
customer.Metadata = params.Metadata
|
|
}
|
|
if params.PromotionCode != nil && promoIDs[*params.PromotionCode] != nil {
|
|
customer.Discount = &stripe.Discount{Coupon: promoIDs[*params.PromotionCode].Coupon}
|
|
}
|
|
if params.Coupon != nil {
|
|
c, ok := m.coupons[*params.Coupon]
|
|
if !ok {
|
|
return nil, &stripe.Error{}
|
|
}
|
|
customer.Discount = &stripe.Discount{Coupon: &stripe.Coupon{ID: c.ID}}
|
|
}
|
|
if params.Balance != nil {
|
|
customer.Balance = *params.Balance
|
|
}
|
|
// TODO update customer with more params as necessary
|
|
|
|
return customer, nil
|
|
}
|
|
|
|
type mockPaymentMethods struct {
|
|
root *mockStripeState
|
|
// attached contains a mapping of customerID to its paymentMethods
|
|
attached map[string][]*stripe.PaymentMethod
|
|
// unattached contains created but not attached paymentMethods
|
|
unattached []*stripe.PaymentMethod
|
|
}
|
|
|
|
func newMockPaymentMethods(root *mockStripeState) *mockPaymentMethods {
|
|
return &mockPaymentMethods{
|
|
root: root,
|
|
attached: map[string][]*stripe.PaymentMethod{},
|
|
}
|
|
}
|
|
|
|
// listContainer implements Stripe's ListContainer interface.
|
|
type listContainer struct {
|
|
listMeta *stripe.ListMeta
|
|
}
|
|
|
|
func newListContainer(meta *stripe.ListMeta) *listContainer {
|
|
return &listContainer{listMeta: meta}
|
|
}
|
|
|
|
func (c *listContainer) GetListMeta() *stripe.ListMeta {
|
|
return c.listMeta
|
|
}
|
|
|
|
func (m *mockPaymentMethods) List(listParams *stripe.PaymentMethodListParams) *paymentmethod.Iter {
|
|
m.root.mu.Lock()
|
|
defer m.root.mu.Unlock()
|
|
|
|
listMeta := &stripe.ListMeta{
|
|
HasMore: false,
|
|
TotalCount: uint32(len(m.attached)),
|
|
}
|
|
lc := newListContainer(listMeta)
|
|
|
|
query := stripe.Query(func(*stripe.Params, *form.Values) ([]interface{}, stripe.ListContainer, error) {
|
|
list, ok := m.attached[*listParams.Customer]
|
|
if !ok {
|
|
list = []*stripe.PaymentMethod{}
|
|
}
|
|
ret := make([]interface{}, len(list))
|
|
|
|
for i, v := range list {
|
|
ret[i] = v
|
|
}
|
|
|
|
return ret, lc, nil
|
|
})
|
|
return &paymentmethod.Iter{Iter: stripe.GetIter(nil, query)}
|
|
}
|
|
|
|
func (m *mockPaymentMethods) New(params *stripe.PaymentMethodParams) (*stripe.PaymentMethod, error) {
|
|
randID := testrand.BucketName()
|
|
id := fmt.Sprintf("pm_card_%s", randID)
|
|
if params.Card.Token != nil {
|
|
switch *params.Card.Token {
|
|
case TestPaymentMethodsNewFailure:
|
|
return nil, &stripe.Error{}
|
|
case TestPaymentMethodsAttachFailure:
|
|
id = TestPaymentMethodsAttachFailure
|
|
case MockInvoicesPayFailure:
|
|
id = MockInvoicesPayFailure
|
|
}
|
|
}
|
|
|
|
newMethod := &stripe.PaymentMethod{
|
|
ID: id,
|
|
Card: &stripe.PaymentMethodCard{
|
|
ExpMonth: 12,
|
|
ExpYear: 2050,
|
|
Brand: "Mastercard",
|
|
Last4: "4444",
|
|
Description: randID,
|
|
},
|
|
Type: stripe.PaymentMethodTypeCard,
|
|
}
|
|
|
|
m.root.mu.Lock()
|
|
defer m.root.mu.Unlock()
|
|
|
|
m.unattached = append(m.unattached, newMethod)
|
|
|
|
return newMethod, nil
|
|
}
|
|
|
|
func (m *mockPaymentMethods) Attach(id string, params *stripe.PaymentMethodAttachParams) (*stripe.PaymentMethod, error) {
|
|
m.root.mu.Lock()
|
|
defer m.root.mu.Unlock()
|
|
|
|
var method *stripe.PaymentMethod
|
|
for _, candidate := range m.unattached {
|
|
if candidate.ID == id {
|
|
if id == TestPaymentMethodsAttachFailure {
|
|
return nil, &stripe.Error{}
|
|
}
|
|
method = candidate
|
|
}
|
|
}
|
|
attached, ok := m.attached[*params.Customer]
|
|
if !ok {
|
|
attached = []*stripe.PaymentMethod{}
|
|
}
|
|
m.attached[*params.Customer] = append(attached, method)
|
|
return method, nil
|
|
}
|
|
|
|
func (m *mockPaymentMethods) Detach(id string, params *stripe.PaymentMethodDetachParams) (*stripe.PaymentMethod, error) {
|
|
m.root.mu.Lock()
|
|
defer m.root.mu.Unlock()
|
|
|
|
var unattached *stripe.PaymentMethod
|
|
for user, userMethods := range m.attached {
|
|
var remaining []*stripe.PaymentMethod
|
|
for _, method := range userMethods {
|
|
if id == method.ID {
|
|
unattached = method
|
|
} else {
|
|
remaining = append(remaining, method)
|
|
}
|
|
}
|
|
m.attached[user] = remaining
|
|
}
|
|
|
|
return unattached, nil
|
|
}
|
|
|
|
type mockInvoices struct {
|
|
root *mockStripeState
|
|
|
|
invoices map[string][]*stripe.Invoice
|
|
invoiceItems *mockInvoiceItems
|
|
}
|
|
|
|
func newMockInvoices(root *mockStripeState, invoiceItems *mockInvoiceItems) *mockInvoices {
|
|
return &mockInvoices{
|
|
root: root,
|
|
invoices: make(map[string][]*stripe.Invoice),
|
|
invoiceItems: invoiceItems,
|
|
}
|
|
}
|
|
|
|
func (m *mockInvoices) New(params *stripe.InvoiceParams) (*stripe.Invoice, error) {
|
|
m.root.mu.Lock()
|
|
defer m.root.mu.Unlock()
|
|
|
|
items, ok := m.invoiceItems.items[*params.Customer]
|
|
if !ok || len(items) == 0 {
|
|
if params.PendingInvoiceItemsBehavior == nil || *params.PendingInvoiceItemsBehavior != "exclude" {
|
|
return nil, &stripe.Error{Code: stripe.ErrorCodeInvoiceNoCustomerLineItems}
|
|
}
|
|
}
|
|
|
|
due := int64(0)
|
|
if params.DueDate != nil {
|
|
due = *params.DueDate
|
|
}
|
|
|
|
amountDue := int64(0)
|
|
lineData := make([]*stripe.InvoiceLine, 0, len(params.InvoiceItems))
|
|
for _, item := range params.InvoiceItems {
|
|
lineData = append(lineData, &stripe.InvoiceLine{
|
|
InvoiceItem: *item.InvoiceItem,
|
|
Amount: *item.Amount,
|
|
})
|
|
amountDue += *item.Amount
|
|
}
|
|
|
|
var desc string
|
|
if params.Description != nil {
|
|
if *params.Description == MockInvoicesNewFailure {
|
|
return nil, &stripe.Error{}
|
|
}
|
|
desc = *params.Description
|
|
}
|
|
|
|
invoice := &stripe.Invoice{
|
|
ID: "in_" + string(testrand.RandAlphaNumeric(25)),
|
|
Customer: &stripe.Customer{ID: *params.Customer},
|
|
DueDate: due,
|
|
Status: stripe.InvoiceStatusDraft,
|
|
Description: desc,
|
|
Lines: &stripe.InvoiceLineList{
|
|
Data: lineData,
|
|
},
|
|
AmountDue: amountDue,
|
|
AmountRemaining: amountDue,
|
|
}
|
|
|
|
m.invoices[*params.Customer] = append(m.invoices[*params.Customer], invoice)
|
|
for _, item := range items {
|
|
if item.Invoice == nil {
|
|
item.Invoice = invoice
|
|
}
|
|
}
|
|
|
|
return invoice, nil
|
|
}
|
|
|
|
func (m *mockInvoices) List(listParams *stripe.InvoiceListParams) *invoice.Iter {
|
|
m.root.mu.Lock()
|
|
defer m.root.mu.Unlock()
|
|
|
|
listMeta := &stripe.ListMeta{
|
|
HasMore: false,
|
|
TotalCount: uint32(len(m.invoices)),
|
|
}
|
|
lc := newListContainer(listMeta)
|
|
|
|
query := stripe.Query(func(*stripe.Params, *form.Values) (ret []interface{}, _ stripe.ListContainer, _ error) {
|
|
if listParams.Customer == nil && listParams.Status != nil {
|
|
// filter by status
|
|
for _, invoices := range m.invoices {
|
|
for _, inv := range invoices {
|
|
if inv.Status == stripe.InvoiceStatus(*listParams.Status) {
|
|
ret = append(ret, inv)
|
|
}
|
|
}
|
|
}
|
|
} else if listParams.Customer == nil {
|
|
for _, invoices := range m.invoices {
|
|
for _, invoice := range invoices {
|
|
ret = append(ret, invoice)
|
|
}
|
|
}
|
|
} else if list, ok := m.invoices[*listParams.Customer]; ok {
|
|
for _, invoice := range list {
|
|
ret = append(ret, invoice)
|
|
}
|
|
}
|
|
|
|
return ret, lc, nil
|
|
})
|
|
return &invoice.Iter{Iter: stripe.GetIter(nil, query)}
|
|
}
|
|
|
|
func (m *mockInvoices) Update(id string, params *stripe.InvoiceParams) (invoice *stripe.Invoice, err error) {
|
|
for _, invoices := range m.invoices {
|
|
for _, invoice := range invoices {
|
|
if invoice.ID == id {
|
|
return invoice, nil
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil, errors.New("invoice not found")
|
|
}
|
|
|
|
// FinalizeInvoice forwards the invoice's status from draft to open.
|
|
func (m *mockInvoices) FinalizeInvoice(id string, params *stripe.InvoiceFinalizeParams) (*stripe.Invoice, error) {
|
|
for _, invoices := range m.invoices {
|
|
for i, invoice := range invoices {
|
|
if invoice.ID == id && invoice.Status == stripe.InvoiceStatusDraft {
|
|
invoice.Status = stripe.InvoiceStatusOpen
|
|
m.invoices[invoice.Customer.ID][i].Status = stripe.InvoiceStatusOpen
|
|
return invoice, nil
|
|
}
|
|
}
|
|
}
|
|
return nil, &stripe.Error{}
|
|
}
|
|
|
|
func (m *mockInvoices) Pay(id string, params *stripe.InvoicePayParams) (*stripe.Invoice, error) {
|
|
for _, invoices := range m.invoices {
|
|
for _, invoice := range invoices {
|
|
if invoice.ID == id {
|
|
if params.PaymentMethod != nil {
|
|
if *params.PaymentMethod == MockInvoicesPayFailure {
|
|
invoice.Status = stripe.InvoiceStatusOpen
|
|
return invoice, &stripe.Error{}
|
|
}
|
|
if *params.PaymentMethod == MockInvoicesPaySuccess {
|
|
invoice.Status = stripe.InvoiceStatusPaid
|
|
invoice.AmountRemaining = 0
|
|
return invoice, nil
|
|
}
|
|
} else if invoice.AmountRemaining == 0 {
|
|
invoice.Status = stripe.InvoiceStatusPaid
|
|
}
|
|
return invoice, nil
|
|
}
|
|
}
|
|
}
|
|
return nil, &stripe.Error{}
|
|
}
|
|
|
|
func (m *mockInvoices) Del(id string, params *stripe.InvoiceParams) (*stripe.Invoice, error) {
|
|
for _, invoices := range m.invoices {
|
|
for i, invoice := range invoices {
|
|
if invoice.ID == id {
|
|
m.invoices[invoice.Customer.ID] = append(m.invoices[invoice.Customer.ID][:i], m.invoices[invoice.Customer.ID][i+1:]...)
|
|
return invoice, nil
|
|
}
|
|
}
|
|
}
|
|
return nil, nil
|
|
}
|
|
|
|
type mockInvoiceItems struct {
|
|
root *mockStripeState
|
|
items map[string][]*stripe.InvoiceItem
|
|
}
|
|
|
|
func newMockInvoiceItems(root *mockStripeState) *mockInvoiceItems {
|
|
return &mockInvoiceItems{
|
|
root: root,
|
|
items: make(map[string][]*stripe.InvoiceItem),
|
|
}
|
|
}
|
|
|
|
func (m *mockInvoiceItems) Update(id string, params *stripe.InvoiceItemParams) (*stripe.InvoiceItem, error) {
|
|
return nil, nil
|
|
}
|
|
|
|
func (m *mockInvoiceItems) Del(id string, params *stripe.InvoiceItemParams) (*stripe.InvoiceItem, error) {
|
|
return nil, nil
|
|
}
|
|
|
|
func (m *mockInvoiceItems) New(params *stripe.InvoiceItemParams) (*stripe.InvoiceItem, error) {
|
|
m.root.mu.Lock()
|
|
defer m.root.mu.Unlock()
|
|
|
|
item := &stripe.InvoiceItem{
|
|
Metadata: params.Metadata,
|
|
}
|
|
if params.Description != nil {
|
|
item.Description = *params.Description
|
|
}
|
|
if params.UnitAmountDecimal != nil {
|
|
item.UnitAmountDecimal = *params.UnitAmountDecimal
|
|
}
|
|
if params.UnitAmount != nil {
|
|
item.UnitAmount = *params.UnitAmount
|
|
}
|
|
if params.Amount != nil {
|
|
item.Amount = *params.Amount
|
|
}
|
|
m.items[*params.Customer] = append(m.items[*params.Customer], item)
|
|
|
|
return item, nil
|
|
}
|
|
|
|
func (m *mockInvoiceItems) List(listParams *stripe.InvoiceItemListParams) *invoiceitem.Iter {
|
|
m.root.mu.Lock()
|
|
defer m.root.mu.Unlock()
|
|
|
|
listMeta := &stripe.ListMeta{
|
|
HasMore: false,
|
|
TotalCount: uint32(len(m.items)),
|
|
}
|
|
lc := newListContainer(listMeta)
|
|
|
|
query := stripe.Query(func(*stripe.Params, *form.Values) ([]interface{}, stripe.ListContainer, error) {
|
|
list, ok := m.items[*listParams.Customer]
|
|
if !ok {
|
|
list = []*stripe.InvoiceItem{}
|
|
}
|
|
ret := make([]interface{}, len(list))
|
|
|
|
for i, v := range list {
|
|
ret[i] = v
|
|
}
|
|
|
|
return ret, lc, nil
|
|
})
|
|
return &invoiceitem.Iter{Iter: stripe.GetIter(nil, query)}
|
|
}
|
|
|
|
type mockCustomerBalanceTransactions struct {
|
|
root *mockStripeState
|
|
transactions map[string][]*stripe.CustomerBalanceTransaction
|
|
}
|
|
|
|
func newMockCustomerBalanceTransactions(root *mockStripeState) *mockCustomerBalanceTransactions {
|
|
return &mockCustomerBalanceTransactions{
|
|
root: root,
|
|
transactions: make(map[string][]*stripe.CustomerBalanceTransaction),
|
|
}
|
|
}
|
|
|
|
func (m *mockCustomerBalanceTransactions) New(params *stripe.CustomerBalanceTransactionParams) (*stripe.CustomerBalanceTransaction, error) {
|
|
m.root.mu.Lock()
|
|
defer m.root.mu.Unlock()
|
|
|
|
if params.Description != nil {
|
|
if *params.Description == MockCBTXsNewFailure {
|
|
return nil, &stripe.Error{}
|
|
}
|
|
}
|
|
tx := &stripe.CustomerBalanceTransaction{
|
|
Type: stripe.CustomerBalanceTransactionTypeAdjustment,
|
|
Amount: *params.Amount,
|
|
Description: *params.Description,
|
|
Metadata: params.Metadata,
|
|
Created: time.Now().Unix(),
|
|
}
|
|
|
|
m.transactions[*params.Customer] = append(m.transactions[*params.Customer], tx)
|
|
|
|
for _, v := range m.root.customers.customers {
|
|
if v.ID == *params.Customer {
|
|
v.Balance += *params.Amount
|
|
tx.EndingBalance = v.Balance
|
|
}
|
|
}
|
|
return tx, nil
|
|
}
|
|
|
|
func (m *mockCustomerBalanceTransactions) List(listParams *stripe.CustomerBalanceTransactionListParams) *customerbalancetransaction.Iter {
|
|
m.root.mu.Lock()
|
|
defer m.root.mu.Unlock()
|
|
|
|
query := stripe.Query(func(p *stripe.Params, b *form.Values) ([]interface{}, stripe.ListContainer, error) {
|
|
txs := m.transactions[*listParams.Customer]
|
|
ret := make([]interface{}, len(txs))
|
|
|
|
for i, v := range txs {
|
|
// stripe returns list of transactions ordered by most recent, so reverse the array.
|
|
ret[len(txs)-1-i] = v
|
|
}
|
|
|
|
listMeta := &stripe.ListMeta{
|
|
TotalCount: uint32(len(txs)),
|
|
}
|
|
|
|
lc := newListContainer(listMeta)
|
|
|
|
return ret, lc, nil
|
|
})
|
|
|
|
return &customerbalancetransaction.Iter{Iter: stripe.GetIter(listParams, query)}
|
|
}
|
|
|
|
type mockCharges struct {
|
|
}
|
|
|
|
func (m *mockCharges) List(listParams *stripe.ChargeListParams) *charge.Iter {
|
|
return &charge.Iter{Iter: stripe.GetIter(listParams, mockEmptyQuery)}
|
|
}
|
|
|
|
type mockPromoCodes struct {
|
|
root *mockStripeState
|
|
|
|
promoCodes map[string]*stripe.PromotionCode
|
|
}
|
|
|
|
func newMockPromoCodes(root *mockStripeState) *mockPromoCodes {
|
|
return &mockPromoCodes{
|
|
root: root,
|
|
promoCodes: testPromoCodes,
|
|
}
|
|
}
|
|
|
|
func (m *mockPromoCodes) List(params *stripe.PromotionCodeListParams) *promotioncode.Iter {
|
|
m.root.mu.Lock()
|
|
defer m.root.mu.Unlock()
|
|
|
|
query := stripe.Query(func(p *stripe.Params, b *form.Values) ([]interface{}, stripe.ListContainer, error) {
|
|
promoCode := m.promoCodes[*params.Code]
|
|
if promoCode == nil {
|
|
return make([]interface{}, 0), &stripe.ListMeta{TotalCount: 0}, nil
|
|
}
|
|
ret := make([]interface{}, 1)
|
|
ret[0] = promoCode
|
|
|
|
listMeta := &stripe.ListMeta{
|
|
TotalCount: 1,
|
|
}
|
|
|
|
lc := newListContainer(listMeta)
|
|
|
|
return ret, lc, nil
|
|
})
|
|
|
|
return &promotioncode.Iter{Iter: stripe.GetIter(params, query)}
|
|
}
|
|
|
|
type mockCreditNotes struct {
|
|
root *mockStripeState
|
|
|
|
CreditNotes map[string]*stripe.CreditNote
|
|
}
|
|
|
|
func newMockCreditNotes(root *mockStripeState) *mockCreditNotes {
|
|
return &mockCreditNotes{
|
|
root: root,
|
|
}
|
|
}
|
|
|
|
func (m mockCreditNotes) New(params *stripe.CreditNoteParams) (*stripe.CreditNote, error) {
|
|
m.root.mu.Lock()
|
|
defer m.root.mu.Unlock()
|
|
|
|
item := &stripe.CreditNote{}
|
|
|
|
if params.Invoice != nil {
|
|
item.ID = *params.Invoice
|
|
}
|
|
if params.Memo != nil {
|
|
item.Memo = *params.Memo
|
|
}
|
|
for _, invoices := range m.root.invoices.invoices {
|
|
for _, invoice := range invoices {
|
|
if invoice.ID == *params.Invoice {
|
|
invoice.AmountRemaining -= *params.Lines[0].UnitAmount
|
|
}
|
|
}
|
|
}
|
|
return item, nil
|
|
}
|