
本文旨在提供一个全面的教程,指导开发者如何在Stripe Checkout会话中正确集成自定义税率和折扣。我们将深入探讨Stripe API中`TaxRate`和`Coupon`对象的创建与应用,纠正常见的参数使用错误,特别是`discounts`参数的错误用法,并提供一个结构化的python代码示例,帮助您高效、准确地处理支付环节中的税务和优惠逻辑。
概述:Stripe Checkout中的税务与折扣处理
Stripe Checkout是一个预构建的、托管的支付页面,用于简化支付流程。在许多业务场景中,我们需要在Checkout会话中应用自定义的税率和折扣,以满足不同地区税务要求或营销活动需求。Stripe API提供了灵活的机制来管理这些元素,主要通过TaxRate对象和Coupon或PromotionCode对象来实现。
正确地将这些对象集成到stripe.checkout.session.create调用中是关键。常见的错误包括参数名称或结构不正确,导致API返回InvalidRequestError。本教程将详细介绍如何避免这些问题,并提供一个实用的代码示例。
关键概念
在深入代码之前,理解以下Stripe对象至关重要:
- Stripe Checkout Session: Stripe提供的一个托管支付页面实例,通过调用stripe.checkout.Session.create创建。
- TaxRate (税率): 代表一个具体的税率,可以包含显示名称、百分比、描述、管辖区等信息。税率可以设置为包含或不包含在价格中(inclusive/exclusive)。
- Coupon (优惠券): 代表一个折扣,可以是固定金额折扣(amount_off)或百分比折扣(percent_off)。优惠券通常与一个优惠活动相关联。
- PromotionCode (促销码): 允许客户在Checkout页面输入代码来应用折扣。一个促销码可以关联一个或多个优惠券。
集成自定义税率
Stripe允许您在Checkout会话中应用一个或多个税率。这些税率可以是预先在Stripe Dashboard中创建的,也可以通过API动态创建。通常,为了效率和管理,建议预先创建常用税率。
1. 创建或获取TaxRate对象
如果您需要动态创建税率,可以使用stripe.TaxRate.create()。如果税率是固定的,可以直接使用其ID。
import stripe # 示例:动态创建税率(通常只在税率变动或首次设置时执行) def create_or_get_tax_rate(display_name, percentage, jurisdiction, inclusive=False): # 检查是否已存在同名/同参数的税率,避免重复创建 # 实际生产中,更好的做法是查询现有税率或从数据库加载预设ID try: # 尝试检索现有税率,这里简化处理,实际可能需要更复杂的查询逻辑 # 例如,通过metadata存储自定义ID,或遍历检索 # 为了演示,我们假设每次都创建,但建议在生产环境中避免 tax_rate = stripe.TaxRate.create( display_name=display_name, description=f"{display_name} Tax", percentage=percentage, jurisdiction=jurisdiction, inclusive=inclusive, ) return tax_rate.id except stripe.error.StripeError as e: print(f"Error creating tax rate: {e}") # 如果是已存在错误,可以尝试检索 # 简化处理,直接返回None或抛出异常 return None # 假设您的订单模型中存储了税率信息 # order.tax.all() 返回一个包含税率信息的集合 # For example: # class Tax(models.Model): # name = models.CharField(max_length=100) # rate = models.DecimalField(max_digits=5, decimal_places=2) # e.g., 5.00 for 5% # jurisdiction = models.CharField(max_length=100, default="US")
2. 将TaxRate应用于Checkout Session
税率可以应用于整个Checkout Session,也可以应用于会话中的单个line_item。当应用于整个会话时,Stripe会将这些税率应用于所有商品行。
在stripe.checkout.Session.create中,通过tax_rates参数传递一个税率ID列表:
# ... (在您的视图或服务函数中) tax_rate_ids = [] for tax_obj in order.tax.all(): # 假设 order.tax.all() 返回您的自定义税率对象 # 在生产环境中,您应该从数据库加载预先创建的Stripe TaxRate ID # 这里为了演示,我们假设 tax_obj 包含创建Stripe TaxRate所需的信息 # 并且我们动态创建或获取其ID tax_rate_id = create_or_get_tax_rate( display_name=tax_obj.name, percentage=float(tax_obj.rate), # 确保是浮点数 jurisdiction=tax_obj.jurisdiction, inclusive=False # 根据您的业务逻辑设置 ) if tax_rate_id: tax_rate_ids.append(tax_rate_id) # ... 在 stripe.checkout.Session.create 中使用 # session = stripe.checkout.Session.create( # # ... 其他参数 # tax_rates=tax_rate_ids, # # ... # )
集成折扣
折扣可以通过Stripe的Coupon或PromotionCode实现。Coupon定义了折扣的具体内容(如减免金额或百分比),而PromotionCode则允许客户通过输入代码来应用这些优惠券。
1. 创建或获取Coupon对象
与税率类似,优惠券也可以通过API动态创建或预先创建。
# 示例:动态创建优惠券(通常只在优惠活动设置时执行) def create_or_get_coupon(name, amount_off_cents=None, percent_off=None, currency='usd', duration='once'): try: if amount_off_cents: coupon = stripe.Coupon.create( amount_off=amount_off_cents, # 以最小货币单位表示,例如美分 duration=duration, # once, forever, or repeating currency=currency, name=name, ) elif percent_off: coupon = stripe.Coupon.create( percent_off=percent_off, duration=duration, currency=currency, # 即使是百分比折扣,也需要货币类型 name=name, ) else: raise ValueError("Must provide either amount_off_cents or percent_off") return coupon.id except stripe.error.StripeError as e: print(f"Error creating coupon: {e}") return None # 假设您的订单模型中存储了折扣信息 # order.discount.all() 返回一个包含折扣信息的集合 # For example: # class Discount(models.Model): # name = models.CharField(max_length=100) # amount = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True) # e.g., 10.00 for $10 # percentage = models.DecimalField(max_digits=5, decimal_places=2, null=True, blank=True) # e.g., 10.00 for 10%
2. 将折扣应用于Checkout Session
在stripe.checkout.Session.create中,通过discounts参数传递一个包含优惠券ID或促销码ID的列表。请注意,这里的参数结构非常重要,也是原始问题中出错的地方。
正确的discounts参数应该是一个字典列表,每个字典包含一个coupon键或一个promotion_code键。
# ... (在您的视图或服务函数中) discount_items = [] for discount_obj in order.discount.all(): # 假设 order.discount.all() 返回您的自定义折扣对象 # 在生产环境中,您应该从数据库加载预先创建的Stripe Coupon ID # 这里为了演示,我们假设 discount_obj 包含创建Stripe Coupon所需的信息 # 并且我们动态创建或获取其ID coupon_id = None if discount_obj.amount: coupon_id = create_or_get_coupon( name=discount_obj.name, amount_off_cents=int(float(discount_obj.amount) * 100), # 转换为美分 currency='usd' # 根据您的货币设置 ) elif discount_obj.percentage: coupon_id = create_or_get_coupon( name=discount_obj.name, percent_off=float(discount_obj.percentage), currency='usd' # 即使是百分比折扣,也需要货币类型 ) if coupon_id: discount_items.append({"coupon": coupon_id}) # 正确的参数结构! # ... 在 stripe.checkout.Session.create 中使用 # session = stripe.checkout.Session.create( # # ... 其他参数 # discounts=discount_items, # # ... # )
原始错误分析: 原始代码中使用了discounts=[{“discounts”: ‘{{COUPON_ID}}’}]。Stripe API期望的是discounts列表中的每个元素是一个字典,该字典直接包含”coupon”或”promotion_code”键,而不是再次嵌套一个”discounts”键。因此,Received unknown parameter: discounts[0][discounts]这个错误提示非常准确。
完整代码示例
以下是一个结合了税率和折扣的Django视图示例,基于您提供的原始代码进行修正和优化。
import stripe from django.conf import settings from django.http import jsonResponse from django.views import View # from .models import Order, Tax, Discount # 假设您有这些模型 # 配置Stripe API密钥 stripe.api_key = settings.STRIPE_SECRET_KEY # 确保在settings.py中配置 # 辅助函数:创建或获取Stripe TaxRate def create_or_get_stripe_tax_rate(display_name, percentage, jurisdiction, inclusive=False): """ 创建一个Stripe TaxRate或返回现有TaxRate的ID。 在生产环境中,建议预先创建TaxRate并存储其ID,而不是每次都动态创建。 """ # 实际应用中,您可能会查询数据库中存储的Stripe TaxRate ID # 这里为演示目的,简化为直接创建 try: tax_rate = stripe.TaxRate.create( display_name=display_name, description=f"{display_name} Tax", percentage=percentage, jurisdiction=jurisdiction, inclusive=inclusive, ) return tax_rate.id except stripe.error.StripeError as e: # 更健壮的错误处理,例如记录日志 print(f"Error creating Stripe TaxRate: {e}") return None # 辅助函数:创建或获取Stripe Coupon def create_or_get_stripe_coupon(name, amount_off_cents=None, percent_off=None, currency='usd', duration='once'): """ 创建一个Stripe Coupon或返回现有Coupon的ID。 在生产环境中,建议预先创建Coupon并存储其ID,而不是每次都动态创建。 """ try: if amount_off_cents is not None: coupon = stripe.Coupon.create( amount_off=amount_off_cents, duration=duration, currency=currency, name=name, ) elif percent_off is not None: coupon = stripe.Coupon.create( percent_off=percent_off, duration=duration, currency=currency, name=name, ) else: raise ValueError("Must provide either amount_off_cents or percent_off") return coupon.id except stripe.error.StripeError as e: print(f"Error creating Stripe Coupon: {e}") return None class CreateCheckoutSessionOrderView(View): def get(self, request, *args, **kwargs): order_id = self.kwargs["order_id"] DOMAIN: str = 'http://127.0.0.1:8000' # 生产环境应使用您的实际域名 # 假设 Order, Tax, Discount 模型已定义并可访问 # from your_app.models import Order, Tax, Discount try: # 替换为您的实际订单获取逻辑 # order = Order.objects.get(id=order_id) # 模拟订单数据 class MockTax: def __init__(self, name, rate, jurisdiction): self.name = name self.rate = rate self.jurisdiction = jurisdiction class MockDiscount: def __init__(self, name, amount=None, percentage=None): self.name = name self.amount = amount self.percentage = percentage class MockOrder: def __init__(self, id, total_cost, name, taxes, discounts): self.id = id self._total_cost = total_cost self._name = name self._taxes = taxes self._discounts = discounts def get_total_cost(self): return self._total_cost def __str__(self): return self._name def tax(self): # 模拟ManyToManyField class TaxManager: def all(self): return self._taxes return TaxManager() def discount(self): # 模拟ManyToManyField class DiscountManager: def all(self): return self._discounts return DiscountManager() # 模拟订单数据 order = MockOrder( id=order_id, total_cost=100.00, # 示例总价 name=f"Order #{order_id}", taxes=[ MockTax(name="Sales Tax", rate=7.5, jurisdiction="US"), # MockTax(name="VAT", rate=20.0, jurisdiction="GB"), ], discounts=[ MockDiscount(name="Welcome Discount", amount=10.00), # 10美元折扣 # MockDiscount(name="Promo 5%", percentage=5.0), # 5%折扣 ] ) except Exception as e: return jsonResponse({'error': str(e)}, status=404) # --- 处理税率 --- tax_rate_ids = [] for tax_obj in order.tax().all(): tax_rate_id = create_or_get_stripe_tax_rate( display_name=tax_obj.name, percentage=float(tax_obj.rate), jurisdiction=tax_obj.jurisdiction, inclusive=False, # 根据您的业务逻辑调整 ) if tax_rate_id: tax_rate_ids.append(tax_rate_id) # --- 处理折扣 --- discount_items = [] for discount_obj in order.discount().all(): coupon_id = None if discount_obj.amount is not None: coupon_id = create_or_get_stripe_coupon( name=discount_obj.name, amount_off_cents=int(float(discount_obj.amount) * 100), currency='usd', ) elif discount_obj.percentage is not None: coupon_id = create_or_get_stripe_coupon( name=discount_obj.name, percent_off=float(discount_obj.percentage), currency='usd', # 即使是百分比折扣,也需要货币类型 ) if coupon_id: discount_items.append({"coupon": coupon_id}) # 正确的参数结构 try: session = stripe.checkout.Session.create( payment_method_types=['card'], line_items=[ { 'price_data': { 'currency': 'usd', 'unit_amount': int(order.get_total_cost() * 100), # 总价转换为美分 'product_data': { 'name': order.__str__(), 'description': f"Purchase for order {order.id}", }, }, 'quantity': 1, # 如果需要对特定商品行应用税率,可以在这里添加 'tax_rates': [...] # 但通常在会话级别应用更常见 }, ], payment_intent_data={ 'metadata': { 'order_id': order.id, }, }, mode='payment', success_url=DOMAIN + '/success/', cancel_url=DOMAIN + '/cancel/', tax_rates=tax_rate_ids, # 应用税率 discounts=discount_items, # 应用折扣 ) return JsonResponse({'id': session.id}) except stripe.error.StripeError as e: # 捕获Stripe API错误 return JsonResponse({'error': str(e)}, status=500) except Exception as e: # 捕获其他未知错误 return JsonResponse({'error': f"An unexpected error occurred: {e}"}, status=500)
注意事项与最佳实践
- Stripe API Key: 确保您的Stripe secret key已正确配置在Django settings.py 中,并被stripe.api_key引用。
- TaxRate 和 Coupon 的生命周期: 在生产环境中,TaxRate和Coupon对象通常是一次性创建的(例如,在Stripe Dashboard中,或通过管理界面/脚本)。然后,您只需在数据库中存储它们的Stripe ID,并在创建Checkout Session时引用这些ID。动态创建(如示例中所示)虽然可行,但在每次用户结账时都调用Stripe API创建新对象会增加延迟和不必要的API请求。
- 错误处理: 务必在api调用周围添加try-except块来捕获stripe.error.StripeError,以便优雅地处理网络问题、无效参数或其他Stripe API返回的错误。
- 货币单位: Stripe API处理金额时通常使用最小货币单位(例如,美元使用美分)。确保在传递unit_amount和amount_off时进行正确的转换(乘以100)。
- **税率的包含性 (