مدیریت سفارش فروشگاه اینترنتی: ماشین حالت پرداخت، لغو و بازپرداخت
7 دقیقه مطالعه

سفارش را با دو ماشین حالت، جدول گذار صریح، رفتار امن در برابر تکرار webhook و آشتی با درگاه طراحی کنید؛ الگوها از مستند Stripe.
سفارش یک فروشگاه اینترنتی «یک رکورد با فیلد وضعیت» به نظر میرسد، تا روزی که مشتری پرداخت میکند اما درگاه دیر جواب میدهد، یا سفارش را لغو میکند در حالی که بسته در انبار آماده شده است. در آن لحظه است که پراکندگی وضعیتها (paid، PAID، done، ok) و شرطهای تودرتو گران تمام میشود. این مقاله یک الگوی مشخص برای چرخهٔ سفارش را پیشنهاد میدهد: ماشین حالت صریح، جداسازی وضعیت سفارش از وضعیت پرداخت، و رفتار امن در برابر تکرار و تأخیر.
مرز مقاله روشن است. الگوهای وضعیت پرداخت، لغو، بازپرداخت و webhook را از مستندات رسمی Stripe میگیریم، چون مستند عمومی و کامل دارد؛ این به معنای پیشنهاد Stripe برای ایران نیست و درگاههای ایرانی ممکن است نامها و رفتار دیگری داشته باشند. الزامات مالی، مالیاتی و حقوقی (مثل صدور صورتحساب، قوانین حق انصراف یا مهلت بازپرداخت) در این مقاله بررسی نشدهاند و باید از منبع رسمی روز و مشاور حقوقی گرفته شوند. هر جا از «توصیهٔ نویسنده» میگوییم، منبع مستقلی ندارد و تجربهٔ مهندسی عمومی است.
دو ماشین حالت، نه یکی
خطای رایج این است که همهٔ چیز در یک فیلد status جمع شود: «در انتظار پرداخت»، «پرداختشده»، «در حال بستهبندی»، «ارسالشده»، «مرجوعی». این فیلد دو چیز متفاوت را قاطی میکند: پول و فیزیک سفارش. توصیهٔ نویسنده این است که حداقل دو وضعیت جدا نگه دارید:
- وضعیت پرداخت که از درگاه میآید و ماشین حالت خودش را دارد.
- وضعیت تحویل سفارش که به انبار و ارسال مربوط است.
Stripe خودش پرداخت را با یک شیء جدا (PaymentIntent) و ماشین حالت جدا مدل میکند. این الگو برای هر درگاهی قابلتقلید است، حتی اگر فقط «شروع»، «موفق» و «ناموفق» داشته باشد.
وضعیتهای پرداخت: درس از مستند Stripe
مستند Stripe شش وضعیت اصلی برای PaymentIntent تعریف میکند: requires_payment_method، requires_confirmation، requires_action (مثلاً احراز هویت 3D Secure)، processing، succeeded و canceled. چند نکته در آن برای هر طراح سفارشی مفید است:
- در
succeededجریان پرداخت کامل است و میتوان سفارش را با اطمینان انجام داد. - اگر تلاش پرداخت (مثلاً بهدلیل رد کارت) شکست بخورد، وضعیت به
requires_payment_methodبرمیگردد تا مشتری دوباره تلاش کند. یعنی «شکست تلاش» لزوماً «شکست سفارش» نیست. - روشهای ناهمگام میتوانند تا چند روز طول بکشند و در این مدت پرداخت قابل تضمین نیست (
processing).
برای فروشگاه شما، معادل سادهاش میتواند این باشد: pending_payment (سفارش ثبت شده، پول نیامده)، payment_processing (درگاه هنوز تعیین تکلیف نکرده)، paid، payment_failed (قابل تلاش مجدد) و payment_canceled. اینکه درگاه ایرانی شما کدام وضعیتها را برمیگرداند باید از مستند همان درگاه بخوانید؛ ما آن را بررسی نکردهایم.
وضعیت سفارش و جدول گذارهای مجاز
برای وضعیت سفارش، یک جدول گذار صریح بنویسید و هر گذار غیرمجاز را در کد رد کنید. نمونهای از پیشنهاد نویسنده (نه استاندارد):
| از | به | رویداد | نکته |
|---|---|---|---|
| created | awaiting_payment | مشتری به پرداخت رفت | موجودی رزرو میشود |
| awaiting_payment | confirmed | پرداخت موفق (تأیید سمت سرور) | فقط از وضعیت پرداخت paid |
| awaiting_payment | canceled | انصراف یا انقضا | رزرو آزاد میشود |
| confirmed | packed | انبار بستهبندی کرد | از اینجا لغو ساده نیست |
| packed | shipped | تحویل به حملونقل | کد رهگیری ثبت میشود |
| shipped | delivered | تحویل مشتری | |
| confirmed / packed | canceled_after_payment | لغو | بازپرداخت لازم میشود |
| delivered | return_requested | درخواست مرجوعی | قواعد تجاری شما + الزامات حقوقی روز |
کلید این جدول «فقط از وضعیت درست» است: سفارش فقط وقتی confirmed میشود که وضعیت پرداخت از سمت سرور paid شده باشد، نه فقط بهخاطر بازگشت مرورگر مشتری از درگاه.

لغو و بازپرداخت
در مستند Stripe، لغو PaymentIntent پیش از رسیدن به processing یا succeeded ممکن است؛ لغو آن را برای تلاشهای بعدی نامعتبر میکند، وجوه نگهداشتهشده را آزاد میکند و قابلبرگشت نیست. پس از موفقیت پرداخت، راه برگرداندن پول API بازپرداخت است. از این دو الگو برای طراحی خودتان میتوان یاد گرفت:
- لغو پیش از پرداخت با بازپرداخت پس از پرداخت دو عمل متفاوتاند و هرکدام گذار و رویداد جدا دارند.
- لغو را غیرقابلبرگشت بگیرید. اگر مشتری دوباره خرید کرد، سفارش یا تلاش پرداخت جدید بسازید.
- بازپرداخت را یک رکورد جدا (با مبلغ، دلیل، وضعیت خودش) ثبت کنید، نه فقط عوضکردن وضعیت سفارش. بازپرداخت جزئی هم ممکن است پیش بیاید.
اینکه بازپرداخت در درگاه شما چگونه و در چه مهلتی انجام میشود، باید از قرارداد و مستند همان درگاه گرفته شود. در این مقاله بررسی نشده است.
وقتی درگاه خبر میدهد: تکرار، ترتیب و سرعت
درگاههایی که رویداد (webhook یا callback) میفرستند، همان مشکلات را دارند. مستند Stripe چند قاعدهٔ دقیق میگوید که مستقل از درگاه مفیدند:
- تکرار: ممکن است یک رویداد چند بار تحویل شود. شناسهٔ رویدادهای پردازششده را ثبت کنید و دوباره پردازش نکنید.
- ترتیب: ترتیب تحویل تضمینشده نیست و فیلد زمان ثانیهای برای تعیین ترتیب مناسب نیست. اگر رویداد «فاکتور پرداخت شد» پیش از «فاکتور ساخته شد» رسید، شیء را از API بخوانید.
- امضا: امضا را با بدنهٔ خام درخواست و secret تأیید کنید؛ بدون این کار هر کس میتواند رویداد جعلی بفرستد و مثلاً سفارش را «پرداختشده» کند.
- سرعت: پیش از هر منطق سنگین سریع پاسخ 2xx بدهید و پردازش را به صف ناهمگام بسپارید.
اگر درگاه شما امضا ندارد (بعضی روشها فقط پارامتر بازگشت مرورگر میدهند)، قاعدهٔ طلایی این است که هرگز فقط به آن پارامتر اعتماد نکنید و وضعیت را با یک فراخوانی سمت سرور به درگاه تأیید (verify) کنید. این توصیهٔ نویسنده است؛ مستند درگاه شما باید روش دقیق را بگوید.
idempotency در ثبت سفارش و پرداخت
Stripe توضیح میدهد که کلید idempotency اجازه میدهد درخواست پس از خطای اتصال بیخطر تکرار شود. سرور پاسخ اولین درخواست با آن کلید را نگه میدارد و اگر پارامترها فرق کنند خطا میدهد. در فروشگاه خودتان:
- هنگام کلیک «پرداخت»، یک شناسهٔ یکتا برای تلاش بسازید تا دو کلیک سریع دو پرداخت نسازد.
- در
confirmedکردن سفارش، با شرط «فقط اگر الان awaiting_payment است» عمل کنید تا دو callback همزمان دوباره موجودی را کم نکنند. - در تماس با انبار و حملونقل هم هر درخواست شناسهٔ یکتا داشته باشد.
برای همگامسازی موجودی با انبار میتوانید جزئیات بیشتر را در مقالهٔ اتصال موجودی بخوانید؛ در این مقاله فقط به ماشین حالت پرداختهایم.
آشتی با دفتر پرداخت
حتی با webhook و idempotency، ممکن است رویدادی گم شود. توصیهٔ نویسنده: یک کار دورهای بنویسید که سفارشهای awaiting_payment و payment_processing قدیمیتر از یک آستانه (که خودتان تعیین میکنید) را از درگاه استعلام کند و تعارضها را گزارش دهد. هر سفارشی که درگاه «موفق» میگوید و شما «در انتظار» دارید، باید بهصورت خودکار یا دستی تعیین تکلیف شود. این کار با پایش و هشدار کامل میشود؛ برای مبانی پایش Monitoring چیست؟ را ببینید.
موارد مرزی که باید تست شوند
- مشتری بعد از پرداخت موفق مرورگر را میبندد (سفارش باید بر اساس webhook یا استعلام تأیید شود).
- دو callback همزمان برای یک سفارش.
- پرداخت موفق بعد از انقضای رزرو (موجودی ممکن است دیگر نباشد؛ تصمیم تجاری لازم است: بازپرداخت یا تأمین).
- لغو سفارش همزمان با بستهبندی در انبار.
- بازپرداخت جزئی و بازپرداخت مجدد.
- رویداد با ترتیب معکوس رسیده است.
چکلیست پیادهسازی
- وضعیت پرداخت و وضعیت تحویل جدا ثبت میشوند.
- جدول گذارهای مجاز در کد اعمال میشود و گذار غیرمجاز خطا میدهد.
- سفارش فقط با تأیید سمت سرور پرداخت تأیید میشود.
- شناسهٔ رویدادهای پردازششده ثبت میشود و پردازش تکراری بیاثر است.
- امضا یا استعلام سمت سرور برای هر callback وجود دارد.
- لغو و بازپرداخت رکورد و رویداد جدا دارند.
- کار آشتی دورهای و هشدار وجود دارد.
- مرزهای حقوقی/مالی (مرجوعی، فاکتور) از منبع رسمی روز بررسی شدهاند، نه از این مقاله.
برای تصمیمهای معماری بزرگتر، مقالهٔ انتخاب معماری اپلیکیشن وب را ببینید.
منابع و مراجع
- How Payment Intents and Setup Intents work — Stripe.
- Receive Stripe events in your webhook endpoint — Stripe.
- Idempotent requests (Stripe API) — Stripe.
نویسنده
سا
سهیل ابراهیمپور بنیانگذار FutureForge است. روی طراحی محصول، معماری و استقرار نرمافزار سفارشی کار میکند.
یادداشتهای مرتبط
برای انتخاب مسیر فنی پروژهتان کمک میخواهید؟
از مسئله و محدودیتهایتان بگویید؛ اگر بتوانیم همراهیتان کنیم، برای گفتوگو هماهنگ میکنیم.




