Payments Domain - Serializers Documentation
Complete documentation of Payments domain serialization layer.
Source: api/nextango/apps/payments/serializers.py
Last Updated: 2026-07-08
TransactionPayment.operator and TransactionPayment.register_id are Django-owned model fields (see Models) that are not included in TransactionPaymentSerializer.Meta.fields, they are not exposed through the standard payments API response.
Table of Contents
- Overview
- Model Serializers
- Simplified Serializers
- Create Serializers
- Field Transformation Patterns
- Reference Handling
Overview
The Payments serializers handle bidirectional data transformation between Django models and Sanity CMS, with validation and nested relationship processing.
Key Serializers:
- 4 Model Serializers: Full serialization for API responses and webhook processing
- 2 Simplified Serializers: Lightweight serializers for nested relationships
- 4 Create Serializers: Validation-focused serializers for creating new records
Architectural Patterns:
- Bidirectional Field Mapping: Automatic camelCase ↔ snake_case conversion
- Reference Resolution: Sanity
_refpattern converted to Django foreign keys - Nested Processing: Automatic handling of one-to-many/many-to-many relationships
- Computed Fields: Counts, totals, and aggregations
- Webhook Integration:
to_internal_value()override for Sanity webhook data - Validation: Range checks, cross-field validation, business rule enforcement
Model Serializers
PaymentProcessorSerializer
Full serialization for PaymentProcessor with computed payment methods count.
Source: serializers.py:8-38
Purpose: Serializes payment processor configuration for API responses and webhook processing.
Fields
class PaymentProcessorSerializer(serializers.ModelSerializer):
payment_methods_count = serializers.SerializerMethodField()
class Meta:
model = PaymentProcessor
fields = [
'id', 'sanity_id', 'name', 'processor_type', 'is_active', 'is_test_mode',
'configuration', 'processing_fee_percentage', 'processing_fee_fixed',
'default_currency', 'payment_methods_count', 'created_at', 'updated_at'
]
read_only_fields = ['id', 'created_at', 'updated_at', 'payment_methods_count']
Model Fields
| Field | Type | Read-Only | Description |
|---|---|---|---|
id | UUID | Yes | Django primary key |
sanity_id | String | No | Sanity document ID |
name | String | No | Processor display name |
processor_type | String | No | Processor type (stripe, square, paypal) |
is_active | Boolean | No | Whether processor is active |
is_test_mode | Boolean | No | Whether in test/sandbox mode |
configuration | JSON | No | Non-sensitive configuration |
processing_fee_percentage | Decimal | No | Fee percentage (e.g., 0.0290 for 2.9%) |
processing_fee_fixed | Decimal | No | Fixed fee per transaction |
default_currency | String | No | 3-letter currency code |
created_at | DateTime | Yes | Creation timestamp |
updated_at | DateTime | Yes | Last update timestamp |
Computed Fields
payment_methods_count
Type: SerializerMethodField (Integer) Read-Only: Yes
Source: serializers.py:22-23
def get_payment_methods_count(self, obj):
return obj.payment_methods.count()
Description: Total number of payment methods using this processor.
Example Output:
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"sanity_id": "processor-stripe-prod",
"name": "Stripe Production",
"processor_type": "stripe",
"is_active": true,
"is_test_mode": false,
"processing_fee_percentage": "0.0290",
"processing_fee_fixed": "0.30",
"default_currency": "USD",
"payment_methods_count": 3,
"created_at": "2025-01-15T10:30:00Z",
"updated_at": "2025-01-15T10:30:00Z"
}
Methods
to_internal_value(data)
Source: serializers.py:25-38
Purpose: Convert Sanity webhook camelCase field names to Django snake_case.
def to_internal_value(self, data):
"""Handle nested Sanity webhook data with field name conversion."""
from ..integrations.sync.field_mappings import bidirectional_field_mapper
# Convert camelCase to snake_case for payments domain
converted_data = {}
for key, value in data.items():
if key in ['_id', '_type', '_rev', '_createdAt', '_updatedAt']:
continue
django_field = bidirectional_field_mapper.get_django_field_name(key, 'payments')
converted_data[django_field] = value
return super().to_internal_value(converted_data)
Field Conversion Examples:
processorType→processor_typeisActive→is_activeisTestMode→is_test_modeprocessingFeePercentage→processing_fee_percentageprocessingFeeFixed→processing_fee_fixeddefaultCurrency→default_currency
Ignored Sanity Fields:
_id: Mapped tosanity_idelsewhere_type: Document type metadata_rev: Revision metadata_createdAt: Mapped tocreated_atelsewhere_updatedAt: Mapped toupdated_atelsewhere
PaymentMethodSerializer
Full serialization for PaymentMethod with store availability and processor details.
Source: serializers.py:41-150
Purpose: Serializes payment methods with computed counts and nested store relationships.
Fields
class PaymentMethodSerializer(serializers.ModelSerializer):
processor_name = serializers.CharField(source='processor.name', read_only=True, required=False)
payments_count = serializers.SerializerMethodField()
available_stores_count = serializers.SerializerMethodField()
available_stores = serializers.SerializerMethodField()
class Meta:
model = PaymentMethod
fields = [
'id', 'sanity_id', 'name', 'method_type', 'processor', 'processor_name',
'is_active', 'is_online_available', 'is_pos_available',
'minimum_amount', 'maximum_amount', 'configuration', 'display_order',
'icon_url', 'description', 'payments_count', 'available_stores_count',
'available_stores', 'created_at', 'updated_at'
]
read_only_fields = [
'id', 'created_at', 'updated_at', 'processor_name',
'payments_count', 'available_stores_count', 'available_stores'
]
Model Fields
| Field | Type | Read-Only | Description |
|---|---|---|---|
id | UUID | Yes | Django primary key |
sanity_id | String | No | Sanity document ID |
name | String | No | Payment method display name |
method_type | String | No | Type (cash, credit_card, etc.) |
processor | UUID | No | Foreign key to PaymentProcessor |
is_active | Boolean | No | Whether method is active |
is_online_available | Boolean | No | Available for online purchases |
is_pos_available | Boolean | No | Available at point-of-sale |
minimum_amount | Decimal | No | Minimum transaction amount |
maximum_amount | Decimal | No | Maximum transaction amount |
configuration | JSON | No | Method-specific configuration |
display_order | Integer | No | UI display order |
icon_url | URL | No | Payment method icon URL |
description | Text | No | Additional description |
created_at | DateTime | Yes | Creation timestamp |
updated_at | DateTime | Yes | Last update timestamp |
Computed Fields
processor_name
Type: CharField (from related object)
Read-Only: Yes
Source: processor.name
processor_name = serializers.CharField(source='processor.name', read_only=True, required=False)
Description: Denormalized processor name for display without extra query.
payments_count
Type: SerializerMethodField (Integer) Read-Only: Yes
Source: serializers.py:63-64
def get_payments_count(self, obj):
return obj.payments.count()
Description: Total number of payments using this method.
available_stores_count
Type: SerializerMethodField (Integer) Read-Only: Yes
Source: serializers.py:66-67
def get_available_stores_count(self, obj):
return obj.available_at_stores.count()
Description: Number of stores where this payment method is available.
available_stores
Type: SerializerMethodField (List of dicts) Read-Only: Yes
Source: serializers.py:69-82
def get_available_stores(self, obj):
"""Return available stores with reference format matching Sanity."""
stores = []
for store_association in obj.available_at_stores.all():
store = store_association.store
stores.append({
"_key": f"store_{store.sanity_id}",
"_ref": store.sanity_id,
"_type": "reference",
"store_id": store.id,
"store_name": store.name,
"is_active": store.is_active
})
return stores
Description: List of stores with Sanity-compatible reference format.
Output Format:
{
"available_stores": [
{
"_key": "store_store-columbus-001",
"_ref": "store-columbus-001",
"_type": "reference",
"store_id": "uuid-here",
"store_name": "Columbus Main Street",
"is_active": true
}
]
}
Sanity Compatibility: The _key, _ref, _type pattern matches Sanity's reference array format for bidirectional sync.
Example Output
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"sanity_id": "payment-method-credit-card",
"name": "Visa/Mastercard",
"method_type": "credit_card",
"processor": "processor-uuid",
"processor_name": "Stripe Production",
"is_active": true,
"is_online_available": true,
"is_pos_available": true,
"minimum_amount": "1.00",
"maximum_amount": null,
"configuration": {
"accepted_brands": ["visa", "mastercard", "amex"],
"requires_cvv": true
},
"display_order": 1,
"icon_url": "https://cdn.example.com/icons/credit-card.png",
"description": "All major credit cards",
"payments_count": 1247,
"available_stores_count": 5,
"available_stores": [
{
"_key": "store_store-001",
"_ref": "store-001",
"_type": "reference",
"store_id": "uuid",
"store_name": "Main Street Store",
"is_active": true
}
],
"created_at": "2025-01-15T10:30:00Z",
"updated_at": "2025-01-15T10:30:00Z"
}
Methods
to_internal_value(data)
Source: serializers.py:84-104
Purpose: Convert Sanity webhook data and resolve processor references.
def to_internal_value(self, data):
"""Handle nested Sanity webhook data with field name conversion."""
from ..integrations.sync.field_mappings import bidirectional_field_mapper
# Convert camelCase to snake_case for payments domain
converted_data = {}
for key, value in data.items():
if key in ['_id', '_type', '_rev', '_createdAt', '_updatedAt']:
continue
django_field = bidirectional_field_mapper.get_django_field_name(key, 'payments')
converted_data[django_field] = value
# Handle processor reference
processor_ref = converted_data.get('processor')
if isinstance(processor_ref, dict) and '_ref' in processor_ref:
PaymentProcessor = apps.get_model('payments', 'PaymentProcessor')
processor_instance = PaymentProcessor.objects.filter(sanity_id=processor_ref['_ref']).first()
converted_data['processor'] = processor_instance.pk if processor_instance else None
return super().to_internal_value(converted_data)
Reference Resolution:
Input (from Sanity webhook):
{
"processor": {
"_ref": "processor-stripe-prod",
"_type": "reference"
}
}
Converted:
{
"processor": UUID("processor-django-pk") # Django foreign key
}
create(validated_data)
Source: serializers.py:106-116
Purpose: Create payment method and process store availability from webhook.
def create(self, validated_data):
"""Create payment method and handle store availability."""
webhook_data = self.context.get('request_data', {})
# Create the payment method instance
payment_method = PaymentMethod.objects.create(**validated_data)
# Process available stores
self._process_available_stores(payment_method, webhook_data.get('availableAtStores', []))
return payment_method
Context Required: request_data in serializer context must contain full webhook payload.
update(instance, validated_data)
Source: serializers.py:118-128
Purpose: Update payment method and process store availability changes.
def update(self, instance, validated_data):
"""Update payment method and handle store availability."""
webhook_data = self.context.get('request_data', {})
# Update the payment method instance
payment_method = super().update(instance, validated_data)
# Process available stores
self._process_available_stores(payment_method, webhook_data.get('availableAtStores', []))
return payment_method
_process_available_stores(payment_method, stores_data)
Source: serializers.py:130-149
Purpose: Process many-to-many store availability relationships from Sanity references.
def _process_available_stores(self, payment_method, stores_data):
"""Process available stores for payment method."""
if not isinstance(stores_data, list):
return
PaymentMethodStore = apps.get_model('payments', 'PaymentMethodStore')
Store = apps.get_model('stores', 'Store')
# Clear existing store associations
payment_method.available_at_stores.all().delete()
# Create new associations
for store_ref in stores_data:
if isinstance(store_ref, dict) and '_ref' in store_ref:
store_instance = Store.objects.filter(sanity_id=store_ref['_ref']).first()
if store_instance:
PaymentMethodStore.objects.create(
payment_method=payment_method,
store=store_instance
)
Input Format (from Sanity):
{
"availableAtStores": [
{
"_key": "key1",
"_ref": "store-001",
"_type": "reference"
},
{
"_key": "key2",
"_ref": "store-002",
"_type": "reference"
}
]
}
Processing:
- Delete all existing PaymentMethodStore associations
- Resolve each store
_refto Django Store instance bysanity_id - Create new PaymentMethodStore junction records
Idempotency: Always clears and recreates associations to match Sanity state.
TransactionPaymentSerializer
Full serialization for TransactionPayment with refund tracking and computed totals.
Source: serializers.py:175-296
Purpose: Serializes payment records with full refund information and processor details.
Fields
class TransactionPaymentSerializer(serializers.ModelSerializer):
transaction_id = serializers.CharField(source='transaction.transactionId', read_only=True, required=False)
payment_method_name = serializers.CharField(source='payment_method.name', read_only=True)
payment_method_type = serializers.CharField(source='payment_method.method_type', read_only=True)
refunds = RefundSerializer(many=True, read_only=True)
refunds_count = serializers.SerializerMethodField()
total_refunded = serializers.SerializerMethodField()
class Meta:
model = TransactionPayment
fields = [
'id', 'sanity_id', 'transaction', 'transaction_id', 'payment_method',
'payment_method_name', 'payment_method_type', 'amount', 'tender_amount',
'change_due', 'status', 'processing_fee', 'net_amount',
'processor_transaction_id', 'processor_response', 'risk_score',
'cash_drawer_event', 'processed_at', 'metadata', 'refunds',
'refunds_count', 'total_refunded', 'created_at', 'updated_at'
]
read_only_fields = [
'id', 'created_at', 'updated_at', 'transaction_id', 'payment_method_name',
'payment_method_type', 'refunds', 'refunds_count', 'total_refunded',
'net_amount', 'change_due'
]
Model Fields
| Field | Type | Read-Only | Description |
|---|---|---|---|
id | UUID | Yes | Django primary key |
sanity_id | String | No | Sanity document ID |
transaction | UUID | No | Foreign key to SaleTransaction |
payment_method | UUID | No | Foreign key to PaymentMethod |
amount | Decimal | No | Payment amount |
tender_amount | Decimal | No | Amount tendered (cash) |
change_due | Decimal | Yes | Change due (auto-calculated) |
status | String | No | Payment status (succeeded/pending/failed) |
processing_fee | Decimal | No | Processor fee amount |
net_amount | Decimal | Yes | Net after fees (auto-calculated) |
processor_transaction_id | String | No | Processor's transaction ID |
processor_response | JSON | No | Full processor response |
risk_score | Decimal | No | Fraud risk score (0-100) |
cash_drawer_event | UUID | No | Foreign key to CashDrawerEvent |
processed_at | DateTime | No | When payment was processed |
metadata | JSON | No | Additional metadata |
created_at | DateTime | Yes | Creation timestamp |
updated_at | DateTime | Yes | Last update timestamp |
Computed Fields
transaction_id
Type: CharField (from related object)
Read-Only: Yes
Source: transaction.transactionId (the SaleTransaction model's camelCase field, not a snake_case transaction_id)
transaction_id = serializers.CharField(source='transaction.transactionId', read_only=True, required=False)
Description: Human-readable transaction ID (e.g., TXN-20251117-ABC123).
payment_method_name
Type: CharField (from related object)
Read-Only: Yes
Source: payment_method.name
payment_method_name = serializers.CharField(source='payment_method.name', read_only=True)
Description: Payment method display name (e.g., "Visa/Mastercard").
payment_method_type
Type: CharField (from related object)
Read-Only: Yes
Source: payment_method.method_type
payment_method_type = serializers.CharField(source='payment_method.method_type', read_only=True)
Description: Payment method type (e.g., "credit_card").
refunds
Type: Nested RefundSerializer (many=True) Read-Only: Yes
refunds = RefundSerializer(many=True, read_only=True)
Description: All refunds associated with this payment (nested full serialization).
refunds_count
Type: SerializerMethodField (Integer) Read-Only: Yes
Source: serializers.py:201-202
def get_refunds_count(self, obj):
return obj.refunds.count()
Description: Total number of refunds for this payment.
total_refunded
Type: SerializerMethodField (Decimal) Read-Only: Yes
Source: serializers.py:204-205
def get_total_refunded(self, obj):
return sum(refund.amount for refund in obj.refunds.filter(status='succeeded'))
Description: Total amount refunded (only succeeded refunds).
Calculation: Sums amount from all refunds with status='succeeded'.
Example Output
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"sanity_id": "payment-abc123",
"transaction": "transaction-uuid",
"transaction_id": "TXN-20251117-ABC123",
"payment_method": "payment-method-uuid",
"payment_method_name": "Visa/Mastercard",
"payment_method_type": "credit_card",
"amount": "125.50",
"tender_amount": null,
"change_due": null,
"status": "succeeded",
"processing_fee": "3.64",
"net_amount": "121.86",
"processor_transaction_id": "ch_3NqXYZ...",
"processor_response": {
"id": "ch_3NqXYZ...",
"status": "succeeded"
},
"risk_score": "12.50",
"cash_drawer_event": null,
"processed_at": "2025-11-17T14:30:00Z",
"metadata": {
"device_id": "POS-001"
},
"refunds": [
{
"id": "refund-uuid",
"refund_id": "REF-20251118-DEF456",
"amount": "25.00",
"status": "succeeded"
}
],
"refunds_count": 1,
"total_refunded": "25.00",
"created_at": "2025-11-17T14:30:00Z",
"updated_at": "2025-11-18T10:15:00Z"
}
Methods
to_internal_value(data)
Source: serializers.py:207-250
Purpose: Convert Sanity webhook data and resolve foreign key references.
def to_internal_value(self, data):
"""Handle nested Sanity webhook data with field name conversion."""
from ..integrations.sync.field_mappings import bidirectional_field_mapper
# Convert camelCase to snake_case for payments domain
converted_data = {}
for key, value in data.items():
if key in ['_id', '_type', '_rev', '_createdAt', '_updatedAt']:
continue
django_field = bidirectional_field_mapper.get_django_field_name(key, 'payments')
converted_data[django_field] = value
# Handle transaction reference
transaction_ref = converted_data.get('transaction')
if isinstance(transaction_ref, dict) and '_ref' in transaction_ref:
SalesTransaction = apps.get_model('transactions', 'SalesTransaction')
transaction_instance = SalesTransaction.objects.filter(sanity_id=transaction_ref['_ref']).first()
converted_data['transaction'] = transaction_instance.pk if transaction_instance else None
# Handle payment method reference
payment_method_ref = converted_data.get('payment_method')
if isinstance(payment_method_ref, dict) and '_ref' in payment_method_ref:
PaymentMethod = apps.get_model('payments', 'PaymentMethod')
payment_method_instance = PaymentMethod.objects.filter(sanity_id=payment_method_ref['_ref']).first()
converted_data['payment_method'] = payment_method_instance.pk if payment_method_instance else None
# Handle cash drawer event reference
cash_drawer_ref = converted_data.get('cash_drawer_event')
if isinstance(cash_drawer_ref, dict) and '_ref' in cash_drawer_ref:
CashDrawerEvent = apps.get_model('transactions', 'CashDrawerEvent')
cash_drawer_instance = CashDrawerEvent.objects.filter(sanity_id=cash_drawer_ref['_ref']).first()
converted_data['cash_drawer_event'] = cash_drawer_instance.pk if cash_drawer_instance else None
# Handle processor response as object
processor_response_ref = converted_data.get('processor_response')
if isinstance(processor_response_ref, dict) and '_ref' in processor_response_ref:
# If it's a reference to another document, store the reference ID
converted_data['processor_response'] = {'reference_id': processor_response_ref['_ref']}
elif isinstance(processor_response_ref, dict):
# If it's inline processor response data, store it directly
converted_data['processor_response'] = processor_response_ref
return super().to_internal_value(converted_data)
Reference Resolution:
transaction: Resolves to SaleTransaction bysanity_idpayment_method: Resolves to PaymentMethod bysanity_idcash_drawer_event: Resolves to CashDrawerEvent bysanity_idprocessor_response: Handles both inline JSON and reference patterns
create(validated_data)
Source: serializers.py:252-262
Purpose: Create payment and process nested refunds from webhook.
def create(self, validated_data):
"""Create payment and handle nested refunds."""
webhook_data = self.context.get('request_data', {})
# Create the payment instance
payment = Payment.objects.create(**validated_data)
# Process refunds
self._process_refunds(payment, webhook_data.get('refunds', []))
return payment
update(instance, validated_data)
Source: serializers.py:264-274
Purpose: Update payment and process refund changes.
def update(self, instance, validated_data):
"""Update payment and handle nested refunds."""
webhook_data = self.context.get('request_data', {})
# Update the payment instance
payment = super().update(instance, validated_data)
# Process refunds
self._process_refunds(payment, webhook_data.get('refunds', []))
return payment
_process_refunds(payment, refunds_data)
Source: serializers.py:276-295
Purpose: Create placeholder refunds from Sanity reference array.
def _process_refunds(self, payment, refunds_data):
"""Process refunds for payment."""
if not isinstance(refunds_data, list):
return
Refund = apps.get_model('payments', 'Refund')
# Process each refund reference
for refund_ref in refunds_data:
if isinstance(refund_ref, dict) and '_ref' in refund_ref:
# Check if refund already exists with this sanity_id
existing_refund = Refund.objects.filter(sanity_id=refund_ref['_ref']).first()
if not existing_refund:
# Create placeholder refund - actual refund data will come via separate webhook
Refund.objects.create(
payment=payment,
amount=0, # Will be updated when refund webhook comes
status='pending',
sanity_id=refund_ref['_ref']
)
Placeholder Pattern:
Sanity webhook may send payment with refund references before refund document webhooks arrive:
{
"refunds": [
{
"_ref": "refund-abc123",
"_type": "reference"
}
]
}
Django creates placeholder:
Refund(
payment=payment_instance,
amount=Decimal('0.00'), # Placeholder
status='pending',
sanity_id='refund-abc123'
)
Later, when refund webhook arrives, it updates the placeholder with actual data.
Idempotency: Checks existing_refund to prevent duplicate creation.
RefundSerializer
Full serialization for Refund with payment context.
Source: serializers.py:152-173
Purpose: Serializes refund records with payment amount and transaction references.
Fields
class RefundSerializer(serializers.ModelSerializer):
payment_amount = serializers.DecimalField(
source='payment.amount',
max_digits=10,
decimal_places=2,
read_only=True
)
transaction_id = serializers.CharField(source='payment.transaction.transactionId', read_only=True)
class Meta:
model = Refund
fields = [
'id', 'refund_id', 'sanity_id', 'payment', 'payment_amount', 'transaction_id',
'amount', 'reason', 'status', 'processor_refund_id', 'processed_at',
'sales_return', 'metadata', 'created_at', 'updated_at'
]
read_only_fields = [
'id', 'refund_id', 'processed_at', 'created_at', 'updated_at', 'payment_amount', 'transaction_id'
]
Model Fields
| Field | Type | Read-Only | Description |
|---|---|---|---|
id | UUID | Yes | Django primary key |
refund_id | String | Yes | Auto-generated ID (REF-YYYYMMDD-XXXXXXXX) |
sanity_id | String | No | Sanity document ID |
payment | UUID | No | Foreign key to TransactionPayment |
amount | Decimal | No | Refund amount |
reason | String | No | Refund reason |
status | String | No | Refund status (succeeded/pending/failed) |
processor_refund_id | String | No | Processor's refund ID |
processed_at | DateTime | Yes | When refund was processed |
sales_return | UUID | No | Foreign key to Return (optional) |
metadata | JSON | No | Additional metadata |
created_at | DateTime | Yes | Creation timestamp |
updated_at | DateTime | Yes | Last update timestamp |
Computed Fields
payment_amount
Type: DecimalField (from related object)
Read-Only: Yes
Source: payment.amount
payment_amount = serializers.DecimalField(
source='payment.amount',
max_digits=10,
decimal_places=2,
read_only=True
)
Description: Original payment amount for comparison with refund amount.
transaction_id
Type: CharField (from related object)
Read-Only: Yes
Source: payment.transaction.transactionId
transaction_id = serializers.CharField(source='payment.transaction.transactionId', read_only=True)
Description: The human-readable transaction ID (e.g. TXN-20260708-ABC123), not the UUID primary key, for tracing a refund back to its originating sale.
Example Output
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"refund_id": "REF-20251118-ABC123",
"sanity_id": "refund-abc123",
"payment": "payment-uuid",
"payment_amount": "125.50",
"transaction_id": "transaction-uuid",
"amount": "25.00",
"reason": "Customer returned one item - defective",
"status": "succeeded",
"processor_refund_id": "re_3NqXYZ...",
"processed_at": "2025-11-18T10:15:00Z",
"sales_return": "return-uuid",
"metadata": {
"initiated_by": "MANAGER-12345"
},
"created_at": "2025-11-18T10:10:00Z",
"updated_at": "2025-11-18T10:15:00Z"
}
Simplified Serializers
PaymentMethodSimpleSerializer
Lightweight serializer for nested payment method references.
Source: serializers.py:299-305
Purpose: Minimal payment method data for embedding in other responses.
Fields
class PaymentMethodSimpleSerializer(serializers.ModelSerializer):
class Meta:
model = PaymentMethod
fields = ['id', 'name', 'method_type', 'is_active']
Fields:
id: UUIDname: Display namemethod_type: Type (cash, credit_card, etc.)is_active: Active status
Use Case: Embedded in transaction/payment responses to show payment method without full details.
Example:
{
"id": "uuid",
"name": "Visa/Mastercard",
"method_type": "credit_card",
"is_active": true
}
PaymentSimpleSerializer
Lightweight serializer for nested payment references.
Source: serializers.py:307-319
Purpose: Minimal payment data for embedding in transaction responses.
Fields
class PaymentSimpleSerializer(serializers.ModelSerializer):
payment_method_name = serializers.CharField(source='payment_method.name', read_only=True)
class Meta:
model = TransactionPayment
fields = [
'id', 'amount', 'status', 'payment_method', 'payment_method_name',
'processing_fee', 'net_amount', 'created_at'
]
read_only_fields = ['payment_method_name']
Fields:
id: UUIDamount: Payment amountstatus: Payment statuspayment_method: Foreign key UUIDpayment_method_name: Denormalized name (read-only)processing_fee: Processor feenet_amount: Net after feescreated_at: Timestamp
Use Case: Embedded in transaction responses to show payments without nested refunds.
Example:
{
"id": "uuid",
"amount": "125.50",
"status": "succeeded",
"payment_method": "payment-method-uuid",
"payment_method_name": "Visa/Mastercard",
"processing_fee": "3.64",
"net_amount": "121.86",
"created_at": "2025-11-17T14:30:00Z"
}
Create Serializers
PaymentProcessorCreateSerializer
Validation-focused serializer for creating payment processors.
Source: serializers.py:321-337
Purpose: Enforces validation rules for processor creation via API.
Fields
class PaymentProcessorCreateSerializer(serializers.ModelSerializer):
class Meta:
model = PaymentProcessor
fields = [
'name', 'processor_type', 'is_active', 'is_test_mode',
'configuration', 'processing_fee_percentage', 'processing_fee_fixed',
'default_currency'
]
Excluded Read-Only Fields: id, sanity_id, created_at, updated_at
Validation
validate_default_currency(value)
Source: serializers.py:332-336
def validate_default_currency(self, value):
"""Validate currency code format."""
if value and len(value) != 3:
raise serializers.ValidationError('Currency code must be exactly 3 characters')
return value.upper() if value else value
Rules:
- Must be exactly 3 characters
- Auto-uppercase for consistency
Examples:
"usd"→"USD""EUR"→"EUR""US"→ ValidationError"EURO"→ ValidationError
PaymentMethodCreateSerializer
Validation-focused serializer for creating payment methods.
Source: serializers.py:339-359
Purpose: Enforces cross-field validation for payment method creation.
Fields
class PaymentMethodCreateSerializer(serializers.ModelSerializer):
class Meta:
model = PaymentMethod
fields = [
'name', 'method_type', 'processor', 'is_active',
'is_online_available', 'is_pos_available', 'minimum_amount',
'maximum_amount', 'configuration', 'display_order',
'icon_url', 'description'
]
Excluded Read-Only Fields: id, sanity_id, created_at, updated_at
Validation
validate(data)
Source: serializers.py:351-358
def validate(self, data):
"""Validate payment method data."""
if data.get('maximum_amount') and data.get('minimum_amount'):
if data['minimum_amount'] > data['maximum_amount']:
raise serializers.ValidationError(
'Minimum amount cannot be greater than maximum amount'
)
return data
Rules:
- If both
minimum_amountandmaximum_amountare provided,minimum_amountmust be ≤maximum_amount
Examples:
min: 1.00, max: 10000.00→min: 1.00, max: null→ (no max)min: 100.00, max: 50.00→ ValidationError
PaymentCreateSerializer
Validation-focused serializer for creating payments.
Source: serializers.py:361-383
Purpose: Enforces validation rules for payment creation via API.
Fields
class PaymentCreateSerializer(serializers.ModelSerializer):
class Meta:
model = TransactionPayment
fields = [
'transaction', 'payment_method', 'amount', 'status',
'processing_fee', 'processor_transaction_id', 'processor_response',
'risk_score', 'cash_drawer_event', 'processed_at', 'metadata'
]
Excluded Read-Only Fields: id, sanity_id, net_amount, change_due, created_at, updated_at
Validation
validate_amount(value)
Source: serializers.py:372-376
def validate_amount(self, value):
"""Validate payment amount."""
if value <= 0:
raise serializers.ValidationError('Payment amount must be greater than 0')
return value
Rules:
- Amount must be > 0
Examples:
100.00→0.01→0.00→ ValidationError-10.00→ ValidationError
validate_risk_score(value)
Source: serializers.py:378-382
def validate_risk_score(self, value):
"""Validate risk score range."""
if value is not None and (value < 0 or value > 100):
raise serializers.ValidationError('Risk score must be between 0 and 100')
return value
Rules:
- If provided, must be between 0 and 100 (inclusive)
Noneis allowed (no risk score)
Examples:
50.00→0.00→100.00→null→150.00→ ValidationError-5.00→ ValidationError
RefundCreateSerializer
Validation-focused serializer for creating refunds.
Source: serializers.py:385-406
Purpose: Enforces validation rules including payment amount limits.
Fields
class RefundCreateSerializer(serializers.ModelSerializer):
class Meta:
model = Refund
fields = [
'payment', 'amount', 'reason', 'status',
'processor_refund_id', 'processed_at', 'metadata'
]
Excluded Read-Only Fields: id, refund_id, sanity_id, created_at, updated_at
Validation
validate(data)
Source: serializers.py:395-405
def validate(self, data):
"""Validate refund data."""
if data['amount'] <= 0:
raise serializers.ValidationError('Refund amount must be greater than 0')
if data.get('payment'):
if data['amount'] > data['payment'].amount:
raise serializers.ValidationError(
'Refund amount cannot exceed original payment amount'
)
return data
Rules:
- Refund
amountmust be > 0 - Refund
amountcannot exceed originalpayment.amount
Examples:
- Original payment: $100, refund: $50 →
- Original payment: $100, refund: $100 → (full refund)
- Original payment: $100, refund: $150 → ValidationError
- Any payment, refund: $0 → ValidationError
Does not check total refunded amount across multiple refunds. That validation should be in the service layer.
Field Transformation Patterns
camelCase → snake_case Conversion
Pattern: All serializers use bidirectional_field_mapper for automatic field name conversion.
Implementation:
from ..integrations.sync.field_mappings import bidirectional_field_mapper
def to_internal_value(self, data):
converted_data = {}
for key, value in data.items():
if key in ['_id', '_type', '_rev', '_createdAt', '_updatedAt']:
continue # Skip Sanity metadata
django_field = bidirectional_field_mapper.get_django_field_name(key, 'payments')
converted_data[django_field] = value
return super().to_internal_value(converted_data)
Example Conversions:
| Sanity (camelCase) | Django (snake_case) |
|---|---|
processorType | processor_type |
isActive | is_active |
isTestMode | is_test_mode |
processingFeePercentage | processing_fee_percentage |
processingFeeFixed | processing_fee_fixed |
defaultCurrency | default_currency |
paymentMethod | payment_method |
tenderAmount | tender_amount |
changeDue | change_due |
processingFee | processing_fee |
netAmount | net_amount |
Ignored Fields: _id, _type, _rev, _createdAt, _updatedAt (Sanity system fields)
Reference Handling
Sanity Reference Pattern
Sanity sends foreign keys as reference objects:
{
"processor": {
"_ref": "processor-stripe-prod",
"_type": "reference"
}
}
Django Resolution Pattern
Serializers resolve references to Django foreign keys:
processor_ref = converted_data.get('processor')
if isinstance(processor_ref, dict) and '_ref' in processor_ref:
PaymentProcessor = apps.get_model('payments', 'PaymentProcessor')
processor_instance = PaymentProcessor.objects.filter(sanity_id=processor_ref['_ref']).first()
converted_data['processor'] = processor_instance.pk if processor_instance else None
Resolution Steps:
- Check if value is dict with
_refkey - Get model using
apps.get_model() - Query by
sanity_idfield - Convert to Django primary key (UUID) or
Noneif not found
Graceful Failure: If reference cannot be resolved, sets field to None instead of raising exception. This allows webhook processing to continue even if referenced document hasn't synced yet.
Array of References Pattern
For many-to-many relationships, Sanity sends array of references:
{
"availableAtStores": [
{
"_key": "key1",
"_ref": "store-001",
"_type": "reference"
},
{
"_key": "key2",
"_ref": "store-002",
"_type": "reference"
}
]
}
Django Processing:
def _process_available_stores(self, payment_method, stores_data):
# Clear existing associations
payment_method.available_at_stores.all().delete()
# Create new associations
for store_ref in stores_data:
if isinstance(store_ref, dict) and '_ref' in store_ref:
store_instance = Store.objects.filter(sanity_id=store_ref['_ref']).first()
if store_instance:
PaymentMethodStore.objects.create(
payment_method=payment_method,
store=store_instance
)
Idempotency: Always clears and recreates to match Sanity state exactly.