ferrox-py-commerce 1.0.0__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.
- ferrox_py_commerce/__init__.py +1 -0
- ferrox_py_commerce/controllers/commerce_api_controller.py +61 -0
- ferrox_py_commerce/controllers/webhooks_controller.py +105 -0
- ferrox_py_commerce/gateways/paypal_gateway.py +23 -0
- ferrox_py_commerce/gateways/stripe_gateway.py +44 -0
- ferrox_py_commerce/models/events.py +43 -0
- ferrox_py_commerce/services/transaction_state.py +47 -0
- ferrox_py_commerce-1.0.0.dist-info/METADATA +53 -0
- ferrox_py_commerce-1.0.0.dist-info/RECORD +10 -0
- ferrox_py_commerce-1.0.0.dist-info/WHEEL +4 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# init
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
from fastapi import Request
|
|
2
|
+
from pydantic import BaseModel
|
|
3
|
+
from typing import List
|
|
4
|
+
from ferrox_py.core.controllers import BaseController
|
|
5
|
+
from ferrox_py.core.container import Container
|
|
6
|
+
from ferrox_py.integrations.payments import CheckoutRequest, LineItem
|
|
7
|
+
from ferrox_py.databases.redis import RedisCacheService
|
|
8
|
+
from ..gateways.stripe_gateway import StripeGateway
|
|
9
|
+
from ..services.transaction_state import TransactionStateService
|
|
10
|
+
|
|
11
|
+
class CartItemPayload(BaseModel):
|
|
12
|
+
name: str
|
|
13
|
+
amount_cents: int
|
|
14
|
+
quantity: int
|
|
15
|
+
|
|
16
|
+
class CheckoutPayload(BaseModel):
|
|
17
|
+
provider: str
|
|
18
|
+
items: List[CartItemPayload]
|
|
19
|
+
currency: str = "eur"
|
|
20
|
+
success_url: str = "http://localhost:3000/success"
|
|
21
|
+
cancel_url: str = "http://localhost:3000/cancel"
|
|
22
|
+
|
|
23
|
+
class CommerceApiController(BaseController):
|
|
24
|
+
"""
|
|
25
|
+
Frontend-facing API for starting payments and managing subscriptions.
|
|
26
|
+
"""
|
|
27
|
+
def __init__(self, container: Container):
|
|
28
|
+
super().__init__(prefix="/commerce", tags=["Commerce API"])
|
|
29
|
+
|
|
30
|
+
self.stripe_gateway = StripeGateway()
|
|
31
|
+
redis = container.resolve("RedisCacheService", RedisCacheService())
|
|
32
|
+
self.state_service = TransactionStateService(redis)
|
|
33
|
+
|
|
34
|
+
@self.router.post("/checkout")
|
|
35
|
+
async def create_checkout(payload: CheckoutPayload):
|
|
36
|
+
"""Creates a checkout session and returns the redirect URL to the frontend."""
|
|
37
|
+
|
|
38
|
+
# Map frontend payload to internal CheckoutRequest
|
|
39
|
+
req = CheckoutRequest(
|
|
40
|
+
amount_cents=sum(i.amount_cents * i.quantity for i in payload.items),
|
|
41
|
+
currency=payload.currency,
|
|
42
|
+
items=[LineItem(name=i.name, amount_cents=i.amount_cents, quantity=i.quantity) for i in payload.items],
|
|
43
|
+
success_url=payload.success_url,
|
|
44
|
+
cancel_url=payload.cancel_url,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
if payload.provider.lower() == "stripe":
|
|
48
|
+
checkout_url = await self.stripe_gateway.create_checkout_session(req)
|
|
49
|
+
|
|
50
|
+
# In a real scenario, you'd extract the Session ID from the Stripe response
|
|
51
|
+
# and initialize its state to PENDING in the State Machine before returning.
|
|
52
|
+
# await self.state_service.transition_state(session_id, "PENDING")
|
|
53
|
+
|
|
54
|
+
return self.ok({"checkout_url": checkout_url}, "Stripe Session Created")
|
|
55
|
+
else:
|
|
56
|
+
return self.bad_request("Unsupported provider")
|
|
57
|
+
|
|
58
|
+
@self.router.get("/invoices")
|
|
59
|
+
async def list_invoices(request: Request):
|
|
60
|
+
"""Returns invoice history (Mock implementation)."""
|
|
61
|
+
return self.ok({"invoices": []}, "Invoices retrieved")
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
from fastapi import Request
|
|
2
|
+
from ferrox_py.core.controllers import BaseController
|
|
3
|
+
from ferrox_py.core.container import Container
|
|
4
|
+
from ferrox_py.core.errors import FerroxError
|
|
5
|
+
from ferrox_py.databases.redis import RedisCacheService
|
|
6
|
+
from ..gateways.stripe_gateway import StripeGateway
|
|
7
|
+
from ..gateways.paypal_gateway import PayPalGateway
|
|
8
|
+
from ..models.events import PaymentSuccessEvent, InvoicePaidEvent, InvoiceFailedEvent
|
|
9
|
+
from ..services.transaction_state import TransactionStateService
|
|
10
|
+
|
|
11
|
+
class CommerceWebhookController(BaseController):
|
|
12
|
+
"""
|
|
13
|
+
The Webhook Standardizer.
|
|
14
|
+
Receives raw webhooks from Stripe and PayPal, mathematically verifies the cryptographic
|
|
15
|
+
signatures to prevent fraud, enforces idempotency, updates transaction state,
|
|
16
|
+
and standardizes them into Ferrox Events.
|
|
17
|
+
"""
|
|
18
|
+
def __init__(self, container: Container):
|
|
19
|
+
super().__init__(prefix="/webhooks/commerce", tags=["Commerce Webhooks"])
|
|
20
|
+
|
|
21
|
+
# Resolve dependencies
|
|
22
|
+
self.stripe_gateway = StripeGateway()
|
|
23
|
+
self.paypal_gateway = PayPalGateway()
|
|
24
|
+
|
|
25
|
+
redis = container.resolve("RedisCacheService", RedisCacheService())
|
|
26
|
+
self.state_service = TransactionStateService(redis)
|
|
27
|
+
|
|
28
|
+
@self.router.post("/stripe")
|
|
29
|
+
async def stripe_webhook(request: Request):
|
|
30
|
+
payload = await request.body()
|
|
31
|
+
sig_header = request.headers.get("stripe-signature")
|
|
32
|
+
|
|
33
|
+
if not sig_header:
|
|
34
|
+
raise FerroxError("Missing Stripe signature", 400)
|
|
35
|
+
|
|
36
|
+
# 1. Cryptographic Validation
|
|
37
|
+
event = self.stripe_gateway.verify_webhook_signature(payload, sig_header)
|
|
38
|
+
|
|
39
|
+
# 2. Idempotency Check (Redis)
|
|
40
|
+
is_duplicate = await self.state_service.check_idempotency(event.id)
|
|
41
|
+
if is_duplicate:
|
|
42
|
+
return self.ok(message="Duplicate event ignored")
|
|
43
|
+
|
|
44
|
+
# 3. Standardization & State Machine
|
|
45
|
+
if event.type == "checkout.session.completed":
|
|
46
|
+
session = event.data.object
|
|
47
|
+
# Transition state to PAID
|
|
48
|
+
valid = await self.state_service.transition_state(session.id, "PAID")
|
|
49
|
+
if valid:
|
|
50
|
+
std_event = PaymentSuccessEvent(
|
|
51
|
+
provider="stripe",
|
|
52
|
+
transaction_id=session.id,
|
|
53
|
+
amount_cents=session.amount_total,
|
|
54
|
+
currency=session.currency,
|
|
55
|
+
customer_email=session.customer_details.email if session.customer_details else None,
|
|
56
|
+
)
|
|
57
|
+
print(f"[EVENT BUS] Emitting: {std_event.model_dump_json()}")
|
|
58
|
+
|
|
59
|
+
elif event.type == "invoice.paid":
|
|
60
|
+
invoice = event.data.object
|
|
61
|
+
std_event = InvoicePaidEvent(
|
|
62
|
+
provider="stripe",
|
|
63
|
+
invoice_id=invoice.id,
|
|
64
|
+
subscription_id=invoice.subscription,
|
|
65
|
+
customer_id=invoice.customer,
|
|
66
|
+
amount_cents=invoice.amount_paid,
|
|
67
|
+
currency=invoice.currency
|
|
68
|
+
)
|
|
69
|
+
print(f"[EVENT BUS] Emitting: {std_event.model_dump_json()}")
|
|
70
|
+
|
|
71
|
+
elif event.type == "invoice.payment_failed":
|
|
72
|
+
invoice = event.data.object
|
|
73
|
+
std_event = InvoiceFailedEvent(
|
|
74
|
+
provider="stripe",
|
|
75
|
+
invoice_id=invoice.id,
|
|
76
|
+
subscription_id=invoice.subscription,
|
|
77
|
+
customer_id=invoice.customer
|
|
78
|
+
)
|
|
79
|
+
print(f"[EVENT BUS] Emitting: {std_event.model_dump_json()}")
|
|
80
|
+
|
|
81
|
+
return self.ok(message="Webhook processed successfully")
|
|
82
|
+
|
|
83
|
+
@self.router.post("/paypal")
|
|
84
|
+
async def paypal_webhook(request: Request):
|
|
85
|
+
headers = dict(request.headers)
|
|
86
|
+
body = await request.json()
|
|
87
|
+
|
|
88
|
+
# 1. Cryptographic Validation
|
|
89
|
+
is_valid = self.paypal_gateway.verify_webhook_signature(headers, body)
|
|
90
|
+
if not is_valid:
|
|
91
|
+
raise FerroxError("Invalid PayPal signature", 400)
|
|
92
|
+
|
|
93
|
+
# 2. Standardization
|
|
94
|
+
if body.get("event_type") == "PAYMENT.CAPTURE.COMPLETED":
|
|
95
|
+
resource = body.get("resource", {})
|
|
96
|
+
std_event = PaymentSuccessEvent(
|
|
97
|
+
provider="paypal",
|
|
98
|
+
transaction_id=resource.get("id"),
|
|
99
|
+
amount_cents=int(float(resource.get("amount", {}).get("value", 0)) * 100),
|
|
100
|
+
currency=resource.get("amount", {}).get("currency_code", "USD"),
|
|
101
|
+
customer_email=None,
|
|
102
|
+
)
|
|
103
|
+
print(f"[EVENT BUS] Emitting: {std_event.model_dump_json()}")
|
|
104
|
+
|
|
105
|
+
return self.ok(message="Webhook received")
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from ferrox_py.core.provider import injectable
|
|
2
|
+
from ferrox_py.integrations.payments import PaymentGateway, CheckoutRequest
|
|
3
|
+
|
|
4
|
+
@injectable()
|
|
5
|
+
class PayPalGateway(PaymentGateway):
|
|
6
|
+
def __init__(self, client_id: str = "", client_secret: str = ""):
|
|
7
|
+
self.client_id = client_id
|
|
8
|
+
self.client_secret = client_secret
|
|
9
|
+
|
|
10
|
+
async def create_checkout_session(self, request: CheckoutRequest) -> str:
|
|
11
|
+
# Mocking PayPal REST API call for creating an order
|
|
12
|
+
print(f"PayPal: Creating order for {sum(i.amount_cents for i in request.items)} cents")
|
|
13
|
+
return "https://www.sandbox.paypal.com/checkoutnow?token=mock_token_123"
|
|
14
|
+
|
|
15
|
+
def verify_webhook_signature(self, headers: dict, body: dict) -> bool:
|
|
16
|
+
"""
|
|
17
|
+
PayPal requires a complex certificate/signature verification.
|
|
18
|
+
In production, this calls PayPal's /v1/notifications/verify-webhook-signature API.
|
|
19
|
+
"""
|
|
20
|
+
# Mock verification
|
|
21
|
+
if "paypal-transmission-sig" in headers:
|
|
22
|
+
return True
|
|
23
|
+
return False
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import stripe
|
|
2
|
+
from ferrox_py.core.provider import injectable
|
|
3
|
+
from ferrox_py.integrations.payments import PaymentGateway, CheckoutRequest
|
|
4
|
+
from ferrox_py.core.errors import FerroxError
|
|
5
|
+
|
|
6
|
+
@injectable()
|
|
7
|
+
class StripeGateway(PaymentGateway):
|
|
8
|
+
def __init__(self, api_key: str = "", webhook_secret: str = ""):
|
|
9
|
+
stripe.api_key = api_key
|
|
10
|
+
self.webhook_secret = webhook_secret
|
|
11
|
+
|
|
12
|
+
async def create_checkout_session(self, request: CheckoutRequest) -> str:
|
|
13
|
+
line_items = [{
|
|
14
|
+
"price_data": {
|
|
15
|
+
"currency": request.currency.lower(),
|
|
16
|
+
"product_data": {"name": item.name},
|
|
17
|
+
"unit_amount": item.amount_cents,
|
|
18
|
+
},
|
|
19
|
+
"quantity": item.quantity,
|
|
20
|
+
} for item in request.items]
|
|
21
|
+
|
|
22
|
+
try:
|
|
23
|
+
session = stripe.checkout.Session.create(
|
|
24
|
+
payment_method_types=["card"],
|
|
25
|
+
line_items=line_items,
|
|
26
|
+
mode="payment",
|
|
27
|
+
success_url=request.success_url,
|
|
28
|
+
cancel_url=request.cancel_url,
|
|
29
|
+
)
|
|
30
|
+
return session.url
|
|
31
|
+
except Exception as e:
|
|
32
|
+
raise FerroxError(f"Stripe Checkout Error: {str(e)}", 500)
|
|
33
|
+
|
|
34
|
+
def verify_webhook_signature(self, payload: bytes, sig_header: str) -> stripe.Event:
|
|
35
|
+
"""
|
|
36
|
+
Cryptographically verifies the HMAC signature of the webhook payload.
|
|
37
|
+
"""
|
|
38
|
+
try:
|
|
39
|
+
event = stripe.Webhook.construct_event(payload, sig_header, self.webhook_secret)
|
|
40
|
+
return event
|
|
41
|
+
except ValueError:
|
|
42
|
+
raise FerroxError("Invalid payload", 400)
|
|
43
|
+
except stripe.error.SignatureVerificationError:
|
|
44
|
+
raise FerroxError("Invalid signature", 400)
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
from pydantic import BaseModel
|
|
2
|
+
from typing import Dict, Any, Optional
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
|
|
5
|
+
class PaymentSuccessEvent(BaseModel):
|
|
6
|
+
"""
|
|
7
|
+
Standardized event emitted when ANY provider successfully processes a payment.
|
|
8
|
+
"""
|
|
9
|
+
provider: str # 'stripe', 'paypal'
|
|
10
|
+
transaction_id: str
|
|
11
|
+
amount_cents: int
|
|
12
|
+
currency: str
|
|
13
|
+
customer_email: Optional[str]
|
|
14
|
+
metadata: Dict[str, str] = {}
|
|
15
|
+
timestamp: datetime = datetime.utcnow()
|
|
16
|
+
|
|
17
|
+
class SubscriptionCreatedEvent(BaseModel):
|
|
18
|
+
"""
|
|
19
|
+
Standardized event for new SaaS subscriptions.
|
|
20
|
+
"""
|
|
21
|
+
provider: str
|
|
22
|
+
subscription_id: str
|
|
23
|
+
customer_id: str
|
|
24
|
+
plan_id: str
|
|
25
|
+
status: str
|
|
26
|
+
|
|
27
|
+
class InvoicePaidEvent(BaseModel):
|
|
28
|
+
"""Event for successful recurring SaaS payments."""
|
|
29
|
+
provider: str
|
|
30
|
+
invoice_id: str
|
|
31
|
+
subscription_id: str
|
|
32
|
+
customer_id: str
|
|
33
|
+
amount_cents: int
|
|
34
|
+
currency: str
|
|
35
|
+
timestamp: datetime = datetime.utcnow()
|
|
36
|
+
|
|
37
|
+
class InvoiceFailedEvent(BaseModel):
|
|
38
|
+
"""Event for failed recurring SaaS payments (card declined)."""
|
|
39
|
+
provider: str
|
|
40
|
+
invoice_id: str
|
|
41
|
+
subscription_id: str
|
|
42
|
+
customer_id: str
|
|
43
|
+
timestamp: datetime = datetime.utcnow()
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
from ferrox_py.core.provider import injectable
|
|
2
|
+
from ferrox_py.core.errors import FerroxError
|
|
3
|
+
from ferrox_py.databases.redis import RedisCacheService
|
|
4
|
+
|
|
5
|
+
@injectable()
|
|
6
|
+
class TransactionStateService:
|
|
7
|
+
def __init__(self, redis: RedisCacheService):
|
|
8
|
+
self.redis = redis
|
|
9
|
+
|
|
10
|
+
async def check_idempotency(self, event_id: str) -> bool:
|
|
11
|
+
"""
|
|
12
|
+
Returns True if the event has already been processed.
|
|
13
|
+
Uses Redis to set a key with a 24-hour expiration.
|
|
14
|
+
"""
|
|
15
|
+
# Ensure redis is connected
|
|
16
|
+
if not self.redis.client:
|
|
17
|
+
await self.redis.connect()
|
|
18
|
+
|
|
19
|
+
key = f"webhook_evt:{event_id}"
|
|
20
|
+
|
|
21
|
+
# setnx returns 1 if key was set (new event), 0 if it already existed
|
|
22
|
+
is_new = await self.redis.client.setnx(key, "processed")
|
|
23
|
+
if is_new:
|
|
24
|
+
# Expire key after 24 hours to free up memory
|
|
25
|
+
await self.redis.client.expire(key, 86400)
|
|
26
|
+
return False
|
|
27
|
+
|
|
28
|
+
return True
|
|
29
|
+
|
|
30
|
+
async def transition_state(self, transaction_id: str, new_state: str) -> bool:
|
|
31
|
+
"""
|
|
32
|
+
Simple State Machine: PENDING -> PAID -> FAILED.
|
|
33
|
+
Returns True if transition is valid and executed.
|
|
34
|
+
"""
|
|
35
|
+
state_key = f"tx_state:{transaction_id}"
|
|
36
|
+
|
|
37
|
+
# In a real app, you would use a Redis Transaction (MULTI/EXEC) or Lua script here
|
|
38
|
+
current_state = await self.redis.get(state_key)
|
|
39
|
+
|
|
40
|
+
if current_state == "PAID":
|
|
41
|
+
# Cannot transition out of PAID via standard webhooks (maybe refund later)
|
|
42
|
+
print(f"Transaction {transaction_id} is already PAID. Ignoring {new_state}.")
|
|
43
|
+
return False
|
|
44
|
+
|
|
45
|
+
await self.redis.set(state_key, new_state, ex=604800) # Keep state for 7 days
|
|
46
|
+
print(f"Transaction {transaction_id} transitioned to {new_state}")
|
|
47
|
+
return True
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: ferrox-py-commerce
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Billing, Subscriptions, and Standardized Webhooks for Ferrox-Py.
|
|
5
|
+
Author: AI-Autistic-Intelligence
|
|
6
|
+
Requires-Python: >=3.11
|
|
7
|
+
Requires-Dist: ferrox-py>=1.0.0
|
|
8
|
+
Requires-Dist: pydantic>=2.0
|
|
9
|
+
Requires-Dist: stripe>=7.0.0
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
|
|
12
|
+
# Ferrox-Py-Commerce
|
|
13
|
+
|
|
14
|
+
## 1. Overview (What does this do?)
|
|
15
|
+
The `ferrox-py-commerce` package is a standardized extension for the Ferrox ecosystem dedicated to handling billing, subscriptions, and standardized payment webhooks. It abstracts away the complexity of dealing directly with external payment providers (like Stripe or PayPal) and guarantees robust, mathematically idempotent transaction handling.
|
|
16
|
+
|
|
17
|
+
## 2. Philosophy (Why does it exist?)
|
|
18
|
+
Handling real money in software is notoriously difficult. E-commerce integrations frequently suffer from race conditions (double charging a user), out-of-order webhook events (an invoice is marked paid before the charge is completed), and severe vendor lock-in. This module exists to decouple your core business logic from the specific syntax and behavior of third-party payment gateways, enforcing strict state machines that make duplicate charges nearly impossible.
|
|
19
|
+
|
|
20
|
+
## 3. Target Audience (Who is it for?)
|
|
21
|
+
This module is intended for backend engineers building SaaS billing systems, subscription platforms, or general e-commerce architectures where financial data integrity and vendor-agnostic infrastructure are critical.
|
|
22
|
+
|
|
23
|
+
## 4. Architecture (How does it work?)
|
|
24
|
+
The Commerce module leverages three main architectural concepts:
|
|
25
|
+
- **Gateways**: Abstract adapters over official provider SDKs (e.g., `StripeGateway`), allowing the core application to initiate payments using uniform interfaces.
|
|
26
|
+
- **Transaction State Machine**: A strict, Redis-backed state machine that enforces mathematical idempotency for incoming webhook events (e.g., ensuring a transaction can only move from `PENDING` to `COMPLETED` once).
|
|
27
|
+
- **Controllers & Webhooks**: Layer 6 endpoints that securely parse incoming events (verifying HMAC signatures) and translate proprietary JSON payloads into standardized Pydantic Domain Events.
|
|
28
|
+
|
|
29
|
+
## 5. Installation / Setup
|
|
30
|
+
Ensure you are using Python 3.11+ and install the package via pip:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pip install ferrox-py-commerce
|
|
34
|
+
```
|
|
35
|
+
You will also need to configure a Redis instance, as it is required by the `TransactionStateService` to manage distributed, atomic locks.
|
|
36
|
+
|
|
37
|
+
## 6. Quickstart (Usage)
|
|
38
|
+
```python
|
|
39
|
+
from ferrox_py.core.container import Container
|
|
40
|
+
from ferrox_py_commerce import CommerceModule
|
|
41
|
+
|
|
42
|
+
# 1. Initialize your IoC container
|
|
43
|
+
container = Container()
|
|
44
|
+
|
|
45
|
+
# 2. Register the Commerce Module
|
|
46
|
+
container.register_module(CommerceModule)
|
|
47
|
+
|
|
48
|
+
# The Webhook controllers, Gateways, and State Machines are now
|
|
49
|
+
# wired and ready to process payments safely!
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## 7. Ecosystem Integration
|
|
53
|
+
The Commerce module integrates seamlessly with the **Security Component** of the core `ferrox-py` framework for distributed Redis locking, and it relies strictly on the **AuthModule** (`ferrox-py-auth`) to ensure that payment intents and subscriptions are accurately linked to a validated, authenticated `User` context.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
ferrox_py_commerce/__init__.py,sha256=4zALCCxfSyzOKNmB_Y-R1A0iUhPpCOrKbkJ1srJWyxo,7
|
|
2
|
+
ferrox_py_commerce/controllers/commerce_api_controller.py,sha256=uG2UVOodRDLYHBJ25heM7IBKmBMiUd-11A4mtOoKPcI,2687
|
|
3
|
+
ferrox_py_commerce/controllers/webhooks_controller.py,sha256=bnXQOTcuIX4rpVIa3_B4CI36Vk0YuQB5wbOh9ahwmXs,4846
|
|
4
|
+
ferrox_py_commerce/gateways/paypal_gateway.py,sha256=mQyssXB1ceZ0LYm3DuIOL2cw83Tud4c-fjhy9lJFMq8,1031
|
|
5
|
+
ferrox_py_commerce/gateways/stripe_gateway.py,sha256=lMaUk-xDTwejxPI9_EIwAP6s2vnnp2Yh-O0wVCSNvCE,1706
|
|
6
|
+
ferrox_py_commerce/models/events.py,sha256=3tm6adaSgFBTJQhuc7CrptftQ7CPy7f92DXnWmB5uVQ,1156
|
|
7
|
+
ferrox_py_commerce/services/transaction_state.py,sha256=_FbVfddyI90amiG4TwwYt8y0VZdHHCIuYtKy6qgbpSU,1853
|
|
8
|
+
ferrox_py_commerce-1.0.0.dist-info/METADATA,sha256=hzEVqtfsL0g7orWkFIbD_WUp1UUo3tiYU487WqZBaNA,3182
|
|
9
|
+
ferrox_py_commerce-1.0.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
10
|
+
ferrox_py_commerce-1.0.0.dist-info/RECORD,,
|