payplus-python 0.1.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- examples/basic_payment.py +29 -0
- examples/fastapi_webhooks.py +130 -0
- examples/subscription_saas.py +206 -0
- payplus/__init__.py +30 -0
- payplus/api/__init__.py +15 -0
- payplus/api/base.py +37 -0
- payplus/api/payment_pages.py +176 -0
- payplus/api/payments.py +117 -0
- payplus/api/recurring.py +216 -0
- payplus/api/transactions.py +203 -0
- payplus/client.py +211 -0
- payplus/exceptions.py +57 -0
- payplus/models/__init__.py +23 -0
- payplus/models/customer.py +136 -0
- payplus/models/invoice.py +242 -0
- payplus/models/payment.py +179 -0
- payplus/models/subscription.py +193 -0
- payplus/models/tier.py +226 -0
- payplus/subscriptions/__init__.py +11 -0
- payplus/subscriptions/billing.py +231 -0
- payplus/subscriptions/manager.py +600 -0
- payplus/subscriptions/storage.py +571 -0
- payplus/webhooks/__init__.py +10 -0
- payplus/webhooks/handler.py +370 -0
- payplus_python-0.1.2.dist-info/METADATA +446 -0
- payplus_python-0.1.2.dist-info/RECORD +31 -0
- payplus_python-0.1.2.dist-info/WHEEL +5 -0
- payplus_python-0.1.2.dist-info/licenses/LICENSE +21 -0
- payplus_python-0.1.2.dist-info/top_level.txt +3 -0
- tests/__init__.py +1 -0
- tests/test_models.py +348 -0
tests/test_models.py
ADDED
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tests for PayPlus SDK models.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
from decimal import Decimal
|
|
7
|
+
from datetime import datetime, timedelta
|
|
8
|
+
|
|
9
|
+
from payplus.models.customer import Customer, CustomerStatus, PaymentMethod
|
|
10
|
+
from payplus.models.subscription import Subscription, SubscriptionStatus, BillingCycle
|
|
11
|
+
from payplus.models.payment import Payment, PaymentStatus
|
|
12
|
+
from payplus.models.invoice import Invoice, InvoiceStatus
|
|
13
|
+
from payplus.models.tier import Tier, TierTemplates
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class TestCustomer:
|
|
17
|
+
"""Tests for Customer model."""
|
|
18
|
+
|
|
19
|
+
def test_create_customer(self):
|
|
20
|
+
customer = Customer(
|
|
21
|
+
id="cust_123",
|
|
22
|
+
email="test@example.com",
|
|
23
|
+
name="Test User",
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
assert customer.id == "cust_123"
|
|
27
|
+
assert customer.email == "test@example.com"
|
|
28
|
+
assert customer.status == CustomerStatus.ACTIVE
|
|
29
|
+
|
|
30
|
+
def test_add_payment_method(self):
|
|
31
|
+
customer = Customer(
|
|
32
|
+
id="cust_123",
|
|
33
|
+
email="test@example.com",
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
pm = customer.add_payment_method(
|
|
37
|
+
token="tok_123",
|
|
38
|
+
card_brand="Visa",
|
|
39
|
+
last_four="4242",
|
|
40
|
+
set_default=True,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
assert len(customer.payment_methods) == 1
|
|
44
|
+
assert pm.token == "tok_123"
|
|
45
|
+
assert pm.is_default is True
|
|
46
|
+
assert customer.default_payment_method_id == pm.id
|
|
47
|
+
|
|
48
|
+
def test_get_default_payment_method(self):
|
|
49
|
+
customer = Customer(
|
|
50
|
+
id="cust_123",
|
|
51
|
+
email="test@example.com",
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
pm1 = customer.add_payment_method(token="tok_1", set_default=True)
|
|
55
|
+
pm2 = customer.add_payment_method(token="tok_2", set_default=False)
|
|
56
|
+
|
|
57
|
+
default = customer.get_default_payment_method()
|
|
58
|
+
assert default.id == pm1.id
|
|
59
|
+
|
|
60
|
+
def test_remove_payment_method(self):
|
|
61
|
+
customer = Customer(
|
|
62
|
+
id="cust_123",
|
|
63
|
+
email="test@example.com",
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
pm = customer.add_payment_method(token="tok_123")
|
|
67
|
+
assert len(customer.payment_methods) == 1
|
|
68
|
+
|
|
69
|
+
result = customer.remove_payment_method(pm.id)
|
|
70
|
+
assert result is True
|
|
71
|
+
assert len(customer.payment_methods) == 0
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class TestSubscription:
|
|
75
|
+
"""Tests for Subscription model."""
|
|
76
|
+
|
|
77
|
+
def test_create_subscription(self):
|
|
78
|
+
subscription = Subscription(
|
|
79
|
+
id="sub_123",
|
|
80
|
+
customer_id="cust_123",
|
|
81
|
+
tier_id="pro",
|
|
82
|
+
amount=Decimal("79.00"),
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
assert subscription.id == "sub_123"
|
|
86
|
+
assert subscription.status == SubscriptionStatus.INCOMPLETE
|
|
87
|
+
|
|
88
|
+
def test_subscription_is_active(self):
|
|
89
|
+
subscription = Subscription(
|
|
90
|
+
id="sub_123",
|
|
91
|
+
customer_id="cust_123",
|
|
92
|
+
tier_id="pro",
|
|
93
|
+
amount=Decimal("79.00"),
|
|
94
|
+
status=SubscriptionStatus.ACTIVE,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
assert subscription.is_active is True
|
|
98
|
+
|
|
99
|
+
def test_subscription_trialing(self):
|
|
100
|
+
subscription = Subscription(
|
|
101
|
+
id="sub_123",
|
|
102
|
+
customer_id="cust_123",
|
|
103
|
+
tier_id="pro",
|
|
104
|
+
amount=Decimal("79.00"),
|
|
105
|
+
status=SubscriptionStatus.TRIALING,
|
|
106
|
+
trial_end=datetime.utcnow() + timedelta(days=14),
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
assert subscription.is_trialing is True
|
|
110
|
+
assert subscription.is_active is True # Trialing is considered active
|
|
111
|
+
|
|
112
|
+
def test_cancel_subscription(self):
|
|
113
|
+
subscription = Subscription(
|
|
114
|
+
id="sub_123",
|
|
115
|
+
customer_id="cust_123",
|
|
116
|
+
tier_id="pro",
|
|
117
|
+
amount=Decimal("79.00"),
|
|
118
|
+
status=SubscriptionStatus.ACTIVE,
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
subscription.cancel(at_period_end=True, reason="Customer request")
|
|
122
|
+
|
|
123
|
+
assert subscription.cancel_at_period_end is True
|
|
124
|
+
assert subscription.cancellation_reason == "Customer request"
|
|
125
|
+
assert subscription.canceled_at is not None
|
|
126
|
+
assert subscription.status == SubscriptionStatus.ACTIVE # Still active until period end
|
|
127
|
+
|
|
128
|
+
def test_cancel_immediately(self):
|
|
129
|
+
subscription = Subscription(
|
|
130
|
+
id="sub_123",
|
|
131
|
+
customer_id="cust_123",
|
|
132
|
+
tier_id="pro",
|
|
133
|
+
amount=Decimal("79.00"),
|
|
134
|
+
status=SubscriptionStatus.ACTIVE,
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
subscription.cancel(at_period_end=False)
|
|
138
|
+
|
|
139
|
+
assert subscription.status == SubscriptionStatus.CANCELED
|
|
140
|
+
assert subscription.ended_at is not None
|
|
141
|
+
|
|
142
|
+
def test_pause_resume(self):
|
|
143
|
+
subscription = Subscription(
|
|
144
|
+
id="sub_123",
|
|
145
|
+
customer_id="cust_123",
|
|
146
|
+
tier_id="pro",
|
|
147
|
+
amount=Decimal("79.00"),
|
|
148
|
+
status=SubscriptionStatus.ACTIVE,
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
subscription.pause()
|
|
152
|
+
assert subscription.status == SubscriptionStatus.PAUSED
|
|
153
|
+
assert subscription.paused_at is not None
|
|
154
|
+
|
|
155
|
+
subscription.resume()
|
|
156
|
+
assert subscription.status == SubscriptionStatus.ACTIVE
|
|
157
|
+
assert subscription.paused_at is None
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
class TestPayment:
|
|
161
|
+
"""Tests for Payment model."""
|
|
162
|
+
|
|
163
|
+
def test_create_payment(self):
|
|
164
|
+
payment = Payment(
|
|
165
|
+
id="pay_123",
|
|
166
|
+
customer_id="cust_123",
|
|
167
|
+
amount=Decimal("100.00"),
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
assert payment.id == "pay_123"
|
|
171
|
+
assert payment.status == PaymentStatus.PENDING
|
|
172
|
+
|
|
173
|
+
def test_mark_succeeded(self):
|
|
174
|
+
payment = Payment(
|
|
175
|
+
id="pay_123",
|
|
176
|
+
customer_id="cust_123",
|
|
177
|
+
amount=Decimal("100.00"),
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
payment.mark_succeeded(transaction_uid="tx_123", approval_number="123456")
|
|
181
|
+
|
|
182
|
+
assert payment.status == PaymentStatus.SUCCEEDED
|
|
183
|
+
assert payment.payplus_transaction_uid == "tx_123"
|
|
184
|
+
assert payment.paid_at is not None
|
|
185
|
+
|
|
186
|
+
def test_mark_failed(self):
|
|
187
|
+
payment = Payment(
|
|
188
|
+
id="pay_123",
|
|
189
|
+
customer_id="cust_123",
|
|
190
|
+
amount=Decimal("100.00"),
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
payment.mark_failed(failure_code="card_declined", failure_message="Card was declined")
|
|
194
|
+
|
|
195
|
+
assert payment.status == PaymentStatus.FAILED
|
|
196
|
+
assert payment.failure_code == "card_declined"
|
|
197
|
+
|
|
198
|
+
def test_refund(self):
|
|
199
|
+
payment = Payment(
|
|
200
|
+
id="pay_123",
|
|
201
|
+
customer_id="cust_123",
|
|
202
|
+
amount=Decimal("100.00"),
|
|
203
|
+
status=PaymentStatus.SUCCEEDED,
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
payment.add_refund(
|
|
207
|
+
refund_id="ref_123",
|
|
208
|
+
amount=Decimal("50.00"),
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
assert payment.amount_refunded == Decimal("50.00")
|
|
212
|
+
assert payment.status == PaymentStatus.PARTIALLY_REFUNDED
|
|
213
|
+
assert payment.net_amount == Decimal("50.00")
|
|
214
|
+
|
|
215
|
+
def test_full_refund(self):
|
|
216
|
+
payment = Payment(
|
|
217
|
+
id="pay_123",
|
|
218
|
+
customer_id="cust_123",
|
|
219
|
+
amount=Decimal("100.00"),
|
|
220
|
+
status=PaymentStatus.SUCCEEDED,
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
payment.add_refund(refund_id="ref_123", amount=Decimal("100.00"))
|
|
224
|
+
|
|
225
|
+
assert payment.status == PaymentStatus.REFUNDED
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
class TestInvoice:
|
|
229
|
+
"""Tests for Invoice model."""
|
|
230
|
+
|
|
231
|
+
def test_create_invoice(self):
|
|
232
|
+
invoice = Invoice(
|
|
233
|
+
id="inv_123",
|
|
234
|
+
customer_id="cust_123",
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
assert invoice.id == "inv_123"
|
|
238
|
+
assert invoice.status == InvoiceStatus.DRAFT
|
|
239
|
+
|
|
240
|
+
def test_add_items(self):
|
|
241
|
+
invoice = Invoice(
|
|
242
|
+
id="inv_123",
|
|
243
|
+
customer_id="cust_123",
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
invoice.add_item(
|
|
247
|
+
item_id="ii_1",
|
|
248
|
+
description="Pro Plan",
|
|
249
|
+
unit_amount=Decimal("79.00"),
|
|
250
|
+
quantity=1,
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
assert len(invoice.items) == 1
|
|
254
|
+
assert invoice.subtotal == Decimal("79.00")
|
|
255
|
+
assert invoice.total == Decimal("79.00")
|
|
256
|
+
|
|
257
|
+
def test_finalize_invoice(self):
|
|
258
|
+
invoice = Invoice(
|
|
259
|
+
id="inv_123",
|
|
260
|
+
customer_id="cust_123",
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
invoice.add_item(
|
|
264
|
+
item_id="ii_1",
|
|
265
|
+
description="Pro Plan",
|
|
266
|
+
unit_amount=Decimal("79.00"),
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
invoice.finalize()
|
|
270
|
+
|
|
271
|
+
assert invoice.status == InvoiceStatus.OPEN
|
|
272
|
+
assert invoice.finalized_at is not None
|
|
273
|
+
|
|
274
|
+
def test_mark_paid(self):
|
|
275
|
+
invoice = Invoice(
|
|
276
|
+
id="inv_123",
|
|
277
|
+
customer_id="cust_123",
|
|
278
|
+
total=Decimal("79.00"),
|
|
279
|
+
amount_due=Decimal("79.00"),
|
|
280
|
+
status=InvoiceStatus.OPEN,
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
invoice.mark_paid(payment_id="pay_123")
|
|
284
|
+
|
|
285
|
+
assert invoice.status == InvoiceStatus.PAID
|
|
286
|
+
assert invoice.payment_id == "pay_123"
|
|
287
|
+
assert invoice.amount_paid == Decimal("79.00")
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
class TestTier:
|
|
291
|
+
"""Tests for Tier model."""
|
|
292
|
+
|
|
293
|
+
def test_create_tier(self):
|
|
294
|
+
tier = Tier(
|
|
295
|
+
id="pro",
|
|
296
|
+
name="Pro",
|
|
297
|
+
price=Decimal("79.00"),
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
assert tier.id == "pro"
|
|
301
|
+
assert tier.price == Decimal("79.00")
|
|
302
|
+
assert tier.billing_cycle == BillingCycle.MONTHLY
|
|
303
|
+
|
|
304
|
+
def test_annual_price(self):
|
|
305
|
+
tier = Tier(
|
|
306
|
+
id="pro",
|
|
307
|
+
name="Pro",
|
|
308
|
+
price=Decimal("79.00"),
|
|
309
|
+
annual_discount_percent=Decimal("20"),
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
annual = tier.get_annual_price()
|
|
313
|
+
expected = Decimal("79.00") * 12 * Decimal("0.80")
|
|
314
|
+
assert annual == expected
|
|
315
|
+
|
|
316
|
+
def test_add_feature(self):
|
|
317
|
+
tier = Tier(
|
|
318
|
+
id="pro",
|
|
319
|
+
name="Pro",
|
|
320
|
+
price=Decimal("79.00"),
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
feature = tier.add_feature(
|
|
324
|
+
feature_id="projects",
|
|
325
|
+
name="Projects",
|
|
326
|
+
included_quantity=100,
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
assert len(tier.features) == 1
|
|
330
|
+
assert tier.has_feature("projects")
|
|
331
|
+
assert tier.get_feature("projects").included_quantity == 100
|
|
332
|
+
|
|
333
|
+
def test_tier_templates(self):
|
|
334
|
+
free = TierTemplates.free()
|
|
335
|
+
assert free.price == Decimal("0")
|
|
336
|
+
|
|
337
|
+
pro = TierTemplates.pro(price=Decimal("99"))
|
|
338
|
+
assert pro.price == Decimal("99")
|
|
339
|
+
assert pro.is_popular is True
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
class TestBillingCycle:
|
|
343
|
+
"""Tests for BillingCycle."""
|
|
344
|
+
|
|
345
|
+
def test_to_interval(self):
|
|
346
|
+
assert BillingCycle.MONTHLY.to_interval() == ("month", 1)
|
|
347
|
+
assert BillingCycle.YEARLY.to_interval() == ("year", 1)
|
|
348
|
+
assert BillingCycle.QUARTERLY.to_interval() == ("month", 3)
|