python-getpaid-core 0.1.0__tar.gz → 0.1.1__tar.gz

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.
Files changed (60) hide show
  1. python_getpaid_core-0.1.1/.pre-commit-config.yaml +39 -0
  2. {python_getpaid_core-0.1.0 → python_getpaid_core-0.1.1}/PKG-INFO +6 -4
  3. {python_getpaid_core-0.1.0 → python_getpaid_core-0.1.1}/README.md +4 -0
  4. {python_getpaid_core-0.1.0 → python_getpaid_core-0.1.1}/docs/conf.py +1 -1
  5. {python_getpaid_core-0.1.0 → python_getpaid_core-0.1.1}/pyproject.toml +21 -7
  6. {python_getpaid_core-0.1.0 → python_getpaid_core-0.1.1}/src/getpaid_core/flow.py +10 -6
  7. {python_getpaid_core-0.1.0 → python_getpaid_core-0.1.1}/src/getpaid_core/fsm.py +20 -2
  8. {python_getpaid_core-0.1.0 → python_getpaid_core-0.1.1}/src/getpaid_core/registry.py +13 -2
  9. {python_getpaid_core-0.1.0 → python_getpaid_core-0.1.1}/tests/test_flow.py +15 -0
  10. {python_getpaid_core-0.1.0 → python_getpaid_core-0.1.1}/tests/test_fsm.py +16 -0
  11. {python_getpaid_core-0.1.0 → python_getpaid_core-0.1.1}/tests/test_registry.py +20 -0
  12. {python_getpaid_core-0.1.0 → python_getpaid_core-0.1.1}/uv.lock +81 -356
  13. python_getpaid_core-0.1.0/.pre-commit-config.yaml +0 -14
  14. {python_getpaid_core-0.1.0 → python_getpaid_core-0.1.1}/.cookiecutter.json +0 -0
  15. {python_getpaid_core-0.1.0 → python_getpaid_core-0.1.1}/.gitattributes +0 -0
  16. {python_getpaid_core-0.1.0 → python_getpaid_core-0.1.1}/.github/dependabot.yml +0 -0
  17. {python_getpaid_core-0.1.0 → python_getpaid_core-0.1.1}/.github/labels.yml +0 -0
  18. {python_getpaid_core-0.1.0 → python_getpaid_core-0.1.1}/.github/release-drafter.yml +0 -0
  19. {python_getpaid_core-0.1.0 → python_getpaid_core-0.1.1}/.github/workflows/constraints.txt +0 -0
  20. {python_getpaid_core-0.1.0 → python_getpaid_core-0.1.1}/.github/workflows/labeler.yml +0 -0
  21. {python_getpaid_core-0.1.0 → python_getpaid_core-0.1.1}/.github/workflows/release.yml +0 -0
  22. {python_getpaid_core-0.1.0 → python_getpaid_core-0.1.1}/.github/workflows/tests.yml +0 -0
  23. {python_getpaid_core-0.1.0 → python_getpaid_core-0.1.1}/.gitignore +0 -0
  24. {python_getpaid_core-0.1.0 → python_getpaid_core-0.1.1}/.plans/2026-02-13-getpaid-core-design.md +0 -0
  25. {python_getpaid_core-0.1.0 → python_getpaid_core-0.1.1}/.plans/2026-02-13-getpaid-core-implementation.md +0 -0
  26. {python_getpaid_core-0.1.0 → python_getpaid_core-0.1.1}/.readthedocs.yml +0 -0
  27. {python_getpaid_core-0.1.0 → python_getpaid_core-0.1.1}/CODE_OF_CONDUCT.md +0 -0
  28. {python_getpaid_core-0.1.0 → python_getpaid_core-0.1.1}/CONTRIBUTING.md +0 -0
  29. {python_getpaid_core-0.1.0 → python_getpaid_core-0.1.1}/LICENSE +0 -0
  30. {python_getpaid_core-0.1.0 → python_getpaid_core-0.1.1}/codecov.yml +0 -0
  31. {python_getpaid_core-0.1.0 → python_getpaid_core-0.1.1}/docs/changelog.md +0 -0
  32. {python_getpaid_core-0.1.0 → python_getpaid_core-0.1.1}/docs/codeofconduct.md +0 -0
  33. {python_getpaid_core-0.1.0 → python_getpaid_core-0.1.1}/docs/concepts.md +0 -0
  34. {python_getpaid_core-0.1.0 → python_getpaid_core-0.1.1}/docs/contributing.md +0 -0
  35. {python_getpaid_core-0.1.0 → python_getpaid_core-0.1.1}/docs/getting-started.md +0 -0
  36. {python_getpaid_core-0.1.0 → python_getpaid_core-0.1.1}/docs/index.md +0 -0
  37. {python_getpaid_core-0.1.0 → python_getpaid_core-0.1.1}/docs/license.md +0 -0
  38. {python_getpaid_core-0.1.0 → python_getpaid_core-0.1.1}/docs/reference.md +0 -0
  39. {python_getpaid_core-0.1.0 → python_getpaid_core-0.1.1}/docs/requirements.txt +0 -0
  40. {python_getpaid_core-0.1.0 → python_getpaid_core-0.1.1}/src/getpaid_core/__init__.py +0 -0
  41. {python_getpaid_core-0.1.0 → python_getpaid_core-0.1.1}/src/getpaid_core/backends/__init__.py +0 -0
  42. {python_getpaid_core-0.1.0 → python_getpaid_core-0.1.1}/src/getpaid_core/backends/dummy.py +0 -0
  43. {python_getpaid_core-0.1.0 → python_getpaid_core-0.1.1}/src/getpaid_core/enums.py +0 -0
  44. {python_getpaid_core-0.1.0 → python_getpaid_core-0.1.1}/src/getpaid_core/exceptions.py +0 -0
  45. {python_getpaid_core-0.1.0 → python_getpaid_core-0.1.1}/src/getpaid_core/processor.py +0 -0
  46. {python_getpaid_core-0.1.0 → python_getpaid_core-0.1.1}/src/getpaid_core/protocols.py +0 -0
  47. {python_getpaid_core-0.1.0 → python_getpaid_core-0.1.1}/src/getpaid_core/py.typed +0 -0
  48. {python_getpaid_core-0.1.0 → python_getpaid_core-0.1.1}/src/getpaid_core/types.py +0 -0
  49. {python_getpaid_core-0.1.0 → python_getpaid_core-0.1.1}/src/getpaid_core/validators.py +0 -0
  50. {python_getpaid_core-0.1.0 → python_getpaid_core-0.1.1}/tests/__init__.py +0 -0
  51. {python_getpaid_core-0.1.0 → python_getpaid_core-0.1.1}/tests/conftest.py +0 -0
  52. {python_getpaid_core-0.1.0 → python_getpaid_core-0.1.1}/tests/test_dummy_backend.py +0 -0
  53. {python_getpaid_core-0.1.0 → python_getpaid_core-0.1.1}/tests/test_enums.py +0 -0
  54. {python_getpaid_core-0.1.0 → python_getpaid_core-0.1.1}/tests/test_exceptions.py +0 -0
  55. {python_getpaid_core-0.1.0 → python_getpaid_core-0.1.1}/tests/test_integration.py +0 -0
  56. {python_getpaid_core-0.1.0 → python_getpaid_core-0.1.1}/tests/test_processor.py +0 -0
  57. {python_getpaid_core-0.1.0 → python_getpaid_core-0.1.1}/tests/test_protocols.py +0 -0
  58. {python_getpaid_core-0.1.0 → python_getpaid_core-0.1.1}/tests/test_public_api.py +0 -0
  59. {python_getpaid_core-0.1.0 → python_getpaid_core-0.1.1}/tests/test_types.py +0 -0
  60. {python_getpaid_core-0.1.0 → python_getpaid_core-0.1.1}/tests/test_validators.py +0 -0
@@ -0,0 +1,39 @@
1
+ repos:
2
+ - repo: local
3
+ hooks:
4
+ - id: ruff
5
+ name: ruff
6
+ entry: uv run ruff check --fix
7
+ language: system
8
+ types: [python]
9
+ - id: ruff-format
10
+ name: ruff-format
11
+ entry: uv run ruff format
12
+ language: system
13
+ types: [python]
14
+ - id: check-toml
15
+ name: check-toml
16
+ entry: uv run check-toml
17
+ language: system
18
+ types: [toml]
19
+ - id: check-yaml
20
+ name: check-yaml
21
+ entry: uv run check-yaml
22
+ language: system
23
+ types: [yaml]
24
+ - id: end-of-file-fixer
25
+ name: end-of-file-fixer
26
+ entry: uv run end-of-file-fixer
27
+ language: system
28
+ types: [text]
29
+ - id: trailing-whitespace
30
+ name: trailing-whitespace
31
+ entry: uv run trailing-whitespace-fixer
32
+ language: system
33
+ types: [text]
34
+ - id: ty
35
+ name: ty
36
+ entry: uv run ty check
37
+ language: system
38
+ types: [python]
39
+ pass_filenames: false
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-getpaid-core
3
- Version: 0.1.0
3
+ Version: 0.1.1
4
4
  Summary: Framework-agnostic payment processing core.
5
5
  Project-URL: Homepage, https://github.com/django-getpaid/python-getpaid-core
6
6
  Project-URL: Repository, https://github.com/django-getpaid/python-getpaid-core
@@ -12,14 +12,12 @@ License-File: LICENSE
12
12
  Classifier: Development Status :: 3 - Alpha
13
13
  Classifier: Intended Audience :: Developers
14
14
  Classifier: License :: OSI Approved :: MIT License
15
- Classifier: Programming Language :: Python :: 3.10
16
- Classifier: Programming Language :: Python :: 3.11
17
15
  Classifier: Programming Language :: Python :: 3.12
18
16
  Classifier: Programming Language :: Python :: 3.13
19
17
  Classifier: Topic :: Office/Business :: Financial
20
18
  Classifier: Topic :: Office/Business :: Financial :: Point-Of-Sale
21
19
  Classifier: Typing :: Typed
22
- Requires-Python: >=3.10
20
+ Requires-Python: >=3.12
23
21
  Requires-Dist: anyio>=4.0
24
22
  Requires-Dist: httpx>=0.27.0
25
23
  Requires-Dist: transitions>=0.9.0
@@ -57,6 +55,10 @@ any web framework:
57
55
 
58
56
  - **[django-getpaid](https://github.com/django-getpaid/django-getpaid)** —
59
57
  Django adapter (models, views, forms, admin)
58
+ - **[fastapi-getpaid](https://github.com/django-getpaid/fastapi-getpaid)** —
59
+ FastAPI adapter (async routes, SQLAlchemy, Pydantic config)
60
+ - **[litestar-getpaid](https://github.com/django-getpaid/litestar-getpaid)** —
61
+ Litestar adapter (controllers, Provide DI, SQLAlchemy, Pydantic config)
60
62
 
61
63
  ## Installation
62
64
 
@@ -30,6 +30,10 @@ any web framework:
30
30
 
31
31
  - **[django-getpaid](https://github.com/django-getpaid/django-getpaid)** —
32
32
  Django adapter (models, views, forms, admin)
33
+ - **[fastapi-getpaid](https://github.com/django-getpaid/fastapi-getpaid)** —
34
+ FastAPI adapter (async routes, SQLAlchemy, Pydantic config)
35
+ - **[litestar-getpaid](https://github.com/django-getpaid/litestar-getpaid)** —
36
+ Litestar adapter (controllers, Provide DI, SQLAlchemy, Pydantic config)
33
37
 
34
38
  ## Installation
35
39
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  project = "getpaid-core"
4
4
  author = "Dominik Kozaczko"
5
- copyright = "2022-2026, Dominik Kozaczko"
5
+ project_copyright = "2022-2026, Dominik Kozaczko"
6
6
 
7
7
  extensions = [
8
8
  "sphinx.ext.autodoc",
@@ -1,19 +1,17 @@
1
1
  [project]
2
2
  name = 'python-getpaid-core'
3
- version = '0.1.0'
3
+ version = "0.1.1"
4
4
  description = 'Framework-agnostic payment processing core.'
5
5
  readme = 'README.md'
6
6
  license = {text = 'MIT'}
7
7
  authors = [
8
8
  {name = 'Dominik Kozaczko', email = 'dominik@kozaczko.info'},
9
9
  ]
10
- requires-python = '>=3.10'
10
+ requires-python = '>=3.12'
11
11
  classifiers = [
12
12
  'Development Status :: 3 - Alpha',
13
13
  'Intended Audience :: Developers',
14
14
  'License :: OSI Approved :: MIT License',
15
- 'Programming Language :: Python :: 3.10',
16
- 'Programming Language :: Python :: 3.11',
17
15
  'Programming Language :: Python :: 3.12',
18
16
  'Programming Language :: Python :: 3.13',
19
17
  'Topic :: Office/Business :: Financial',
@@ -32,11 +30,13 @@ dev = [
32
30
  'pytest-asyncio>=0.24.0',
33
31
  'pytest-cov>=5.0',
34
32
  'respx>=0.22.0',
35
- 'ruff>=0.9.0',
36
- 'pre-commit>=4.0',
33
+ "ruff>=0.9.0",
34
+ "pre-commit>=4.0",
37
35
  "furo>=2025.12.19",
38
36
  "sphinx>=8.1.3",
39
37
  "myst-parser>=4.0.1",
38
+ "pre-commit-hooks>=6.0.0",
39
+ "ty>=0.0.16",
40
40
  ]
41
41
 
42
42
  [project.urls]
@@ -64,7 +64,7 @@ source = ['getpaid_core']
64
64
  show_missing = true
65
65
 
66
66
  [tool.ruff]
67
- target-version = 'py310'
67
+ target-version = 'py312'
68
68
  line-length = 80
69
69
  src = ['src', 'tests']
70
70
 
@@ -94,3 +94,17 @@ ignore = [
94
94
  force-single-line = true
95
95
  lines-after-imports = 2
96
96
  known-first-party = ['getpaid_core']
97
+
98
+ # --- ty type checker ---
99
+ [tool.ty.environment]
100
+ python-version = '3.12'
101
+
102
+ [tool.ty.terminal]
103
+ error-on-warning = true
104
+
105
+ # Relax rules for test files: FSM trigger methods are dynamically attached
106
+ [[tool.ty.overrides]]
107
+ include = ['tests/**']
108
+ [tool.ty.overrides.rules]
109
+ unresolved-attribute = 'ignore'
110
+ invalid-argument-type = 'ignore'
@@ -58,7 +58,7 @@ class PaymentFlow:
58
58
  create_payment_machine(payment)
59
59
  processor = self._get_processor(payment)
60
60
  result = await processor.prepare_transaction(**kwargs)
61
- payment.confirm_prepared()
61
+ payment.confirm_prepared() # ty: ignore[unresolved-attribute] # FSM trigger attached by create_payment_machine
62
62
  await self.repository.save(payment)
63
63
  return result
64
64
 
@@ -91,7 +91,11 @@ class PaymentFlow:
91
91
  )
92
92
  trigger = getattr(payment, callback, None)
93
93
  if trigger and callable(trigger):
94
- trigger()
94
+ trigger_kwargs = {}
95
+ amount = response.get("amount")
96
+ if amount is not None:
97
+ trigger_kwargs["amount"] = amount
98
+ trigger(**trigger_kwargs)
95
99
  await self.repository.save(payment)
96
100
  return payment
97
101
 
@@ -101,7 +105,7 @@ class PaymentFlow:
101
105
  create_payment_machine(payment)
102
106
  result = await processor.charge(amount=amount, **kwargs)
103
107
  if result["success"]:
104
- payment.confirm_charge_sent()
108
+ payment.confirm_charge_sent() # ty: ignore[unresolved-attribute] # FSM trigger attached by create_payment_machine
105
109
  await self.repository.save(payment)
106
110
  return result
107
111
 
@@ -110,7 +114,7 @@ class PaymentFlow:
110
114
  processor = self._get_processor(payment)
111
115
  create_payment_machine(payment)
112
116
  amount = await processor.release_lock(**kwargs)
113
- payment.release_lock()
117
+ payment.release_lock() # ty: ignore[unresolved-attribute] # FSM trigger attached by create_payment_machine
114
118
  await self.repository.save(payment)
115
119
  return amount
116
120
 
@@ -119,7 +123,7 @@ class PaymentFlow:
119
123
  processor = self._get_processor(payment)
120
124
  create_payment_machine(payment)
121
125
  refund_amount = await processor.start_refund(amount=amount, **kwargs)
122
- payment.start_refund()
126
+ payment.start_refund() # ty: ignore[unresolved-attribute] # FSM trigger attached by create_payment_machine
123
127
  await self.repository.save(payment)
124
128
  return refund_amount
125
129
 
@@ -129,7 +133,7 @@ class PaymentFlow:
129
133
  create_payment_machine(payment)
130
134
  success = await processor.cancel_refund(**kwargs)
131
135
  if success:
132
- payment.cancel_refund()
136
+ payment.cancel_refund() # ty: ignore[unresolved-attribute] # FSM trigger attached by create_payment_machine
133
137
  await self.repository.save(payment)
134
138
  return success
135
139
 
@@ -49,6 +49,15 @@ def _accumulate_paid_amount(event_data):
49
49
  model.amount_paid += amount
50
50
 
51
51
 
52
+ def _accumulate_refunded_amount(event_data):
53
+ """After confirm_refund: accumulate refunded amount on the payment."""
54
+ model = event_data.model
55
+ amount = event_data.kwargs.get("amount", None)
56
+ if amount is None:
57
+ amount = model.amount_paid - model.amount_refunded
58
+ model.amount_refunded += amount
59
+
60
+
52
61
  PAYMENT_TRANSITIONS = [
53
62
  {
54
63
  "trigger": "confirm_prepared",
@@ -105,6 +114,7 @@ PAYMENT_TRANSITIONS = [
105
114
  "trigger": "confirm_refund",
106
115
  "source": PaymentStatus.REFUND_STARTED,
107
116
  "dest": PaymentStatus.PARTIAL,
117
+ "after": _accumulate_refunded_amount,
108
118
  },
109
119
  {
110
120
  "trigger": "mark_as_refunded",
@@ -174,11 +184,14 @@ def create_payment_machine(payment) -> Machine:
174
184
  The transitions library adds trigger methods directly to the
175
185
  object (confirm_prepared, confirm_lock, fail, etc.).
176
186
  """
187
+ initial = (
188
+ PaymentStatus(payment.status) if payment.status else PaymentStatus.NEW
189
+ )
177
190
  return Machine(
178
191
  model=payment,
179
192
  states=PaymentStatus,
180
193
  transitions=PAYMENT_TRANSITIONS,
181
- initial=payment.status or PaymentStatus.NEW,
194
+ initial=initial,
182
195
  model_attribute="status",
183
196
  auto_transitions=False,
184
197
  send_event=True,
@@ -193,11 +206,16 @@ def _store_fraud_message(event):
193
206
 
194
207
  def create_fraud_machine(payment) -> Machine:
195
208
  """Attach fraud status FSM to a payment object."""
209
+ initial = (
210
+ FraudStatus(payment.fraud_status)
211
+ if payment.fraud_status
212
+ else FraudStatus.UNKNOWN
213
+ )
196
214
  return Machine(
197
215
  model=payment,
198
216
  states=FraudStatus,
199
217
  transitions=FRAUD_TRANSITIONS,
200
- initial=payment.fraud_status or FraudStatus.UNKNOWN,
218
+ initial=initial,
201
219
  model_attribute="fraud_status",
202
220
  auto_transitions=False,
203
221
  send_event=True,
@@ -27,12 +27,12 @@ class PluginRegistry:
27
27
  if isinstance(processor_class, type) and issubclass(
28
28
  processor_class, BaseProcessor
29
29
  ):
30
- self._backends[processor_class.slug] = processor_class
30
+ self._register_backend(processor_class)
31
31
  self._discovered = True
32
32
 
33
33
  def register(self, processor_class: type[BaseProcessor]) -> None:
34
34
  """Manual registration for testing or dynamic use."""
35
- self._backends[processor_class.slug] = processor_class
35
+ self._register_backend(processor_class)
36
36
 
37
37
  def unregister(self, slug: str) -> None:
38
38
  """Remove a backend by slug."""
@@ -70,5 +70,16 @@ class PluginRegistry:
70
70
  if not self._discovered:
71
71
  self.discover()
72
72
 
73
+ def _register_backend(self, processor_class: type[BaseProcessor]) -> None:
74
+ slug = processor_class.slug
75
+ existing = self._backends.get(slug)
76
+ if existing is not None and existing is not processor_class:
77
+ raise ValueError(
78
+ f"Duplicate backend slug {slug!r}: "
79
+ f"{existing.__module__}.{existing.__name__} and "
80
+ f"{processor_class.__module__}.{processor_class.__name__}"
81
+ )
82
+ self._backends[slug] = processor_class
83
+
73
84
 
74
85
  registry = PluginRegistry()
@@ -109,6 +109,21 @@ class TestFetchAndUpdateStatus:
109
109
  ):
110
110
  await flow.fetch_and_update_status(payment)
111
111
 
112
+ @pytest.mark.asyncio
113
+ async def test_pull_passes_amount_to_transition(self, flow):
114
+ payment = MockPayment(backend="mock", status=PaymentStatus.PREPARED)
115
+ with patch.object(
116
+ MockProcessor,
117
+ "fetch_payment_status",
118
+ new_callable=AsyncMock,
119
+ return_value={
120
+ "status": "confirm_payment",
121
+ "amount": Decimal("40.00"),
122
+ },
123
+ ):
124
+ result = await flow.fetch_and_update_status(payment)
125
+ assert result.amount_paid == Decimal("40.00")
126
+
112
127
 
113
128
  class TestCharge:
114
129
  @pytest.mark.asyncio
@@ -144,6 +144,22 @@ class TestPaymentRefundFlow:
144
144
  p.confirm_refund()
145
145
  assert p.status == PaymentStatus.PARTIAL
146
146
 
147
+ def test_confirm_refund_accumulates_amount(self):
148
+ p = MockPayment(status=PaymentStatus.REFUND_STARTED)
149
+ p.amount_paid = 100
150
+ create_payment_machine(p)
151
+ p.confirm_refund(amount=30)
152
+ assert p.status == PaymentStatus.PARTIAL
153
+ assert p.amount_refunded == 30
154
+
155
+ def test_confirm_refund_defaults_to_remaining_amount(self):
156
+ p = MockPayment(status=PaymentStatus.REFUND_STARTED)
157
+ p.amount_paid = 100
158
+ p.amount_refunded = 40
159
+ create_payment_machine(p)
160
+ p.confirm_refund()
161
+ assert p.amount_refunded == 100
162
+
147
163
  def test_mark_as_refunded_when_fully_refunded(self):
148
164
  p = MockPayment(status=PaymentStatus.PARTIAL)
149
165
  p.amount_paid = 100
@@ -55,6 +55,20 @@ class EURProcessor(BaseProcessor):
55
55
  )
56
56
 
57
57
 
58
+ class DuplicatePLNProcessor(BaseProcessor):
59
+ slug = "pln-pay"
60
+ display_name = "Duplicate PLN"
61
+ accepted_currencies = ["PLN"]
62
+
63
+ async def prepare_transaction(self, **kwargs):
64
+ return TransactionResult(
65
+ redirect_url=None,
66
+ form_data=None,
67
+ method="REST",
68
+ headers={},
69
+ )
70
+
71
+
58
72
  # -- Tests --
59
73
 
60
74
 
@@ -71,6 +85,12 @@ class TestManualRegistration:
71
85
  assert reg.get_by_slug("pln-pay") is PLNProcessor
72
86
  assert reg.get_by_slug("eur-pay") is EURProcessor
73
87
 
88
+ def test_register_duplicate_slug_raises(self):
89
+ reg = PluginRegistry()
90
+ reg.register(PLNProcessor)
91
+ with pytest.raises(ValueError, match="Duplicate backend slug"):
92
+ reg.register(DuplicatePLNProcessor)
93
+
74
94
  def test_unregister(self):
75
95
  reg = PluginRegistry()
76
96
  reg.register(PLNProcessor)