الأبحاث · جولةُ البرمجة الأولى
CB-3-01 — تتبّع خطأ وإصلاحه · Python · عالٍ
جولةُ البرمجة الأولى · 2026-10-16 · GAIBRA v2.31.03 · واجبُ التشغيل
| الترتيب | المتنافس | الرمز أمام المحكّم | الكلمات | الاختبارات | Opus | Grok | المجموع | الصدق (Opus · Grok) |
|---|---|---|---|---|---|---|---|---|
| 1 | GAIBRA | أ | 1,608 | 14/14 | 91.0 | 99.0 | 95.0 | 11 · 12 |
| 2 | ChatGPT | ب | 818 | 14/14 | 87.0 | 87.0 | 87.0 | 11 · 10 |
| 2 | DeepSeek | هـ | 794 | 14/14 | 82.0 | 92.0 | 87.0 | 6 · 11 |
| 4 | Claude Sonnet | د | 653 | 14/14 | 87.0 | 82.0 | 84.5 | 10 · 8 |
| 5 | Gemini | ج | 475 | 14/14 | 74.0 | 70.0 | 72.0 | 3 · 3 |
نصُّ السيناريو كما كتبه المولّد
عندنا متجر إلكتروني (سلة + بوابة دفع محلية بالريال السعودي). منذ أسبوعين ترفض بوابة الدفع نحو 3% من الطلبات برسالة AMOUNT\_MISMATCH، ورسالتها بالفلس (الهللة): JSON {"error": "AMOUNT\_MISMATCH", "expected\_halalas": 14999, "received\_halalas": 15000} العميل يدفع من الواجهة مبلغاً صحيحاً (150.00 ريال)، لكن ما نرسله للبوابة بعد التحويل يصبح 14999 أو 15001 هللة أحياناً. حدث هذا أكثر مع سلال فيها أكثر من خصم متراكب. هذا الكود المسؤول (checkout.py)، بدأنا نشك بجمع float لكن أي محاولة لاستخدام round() جعلت النتائج أسوأ: Python def compute\_order\_total(items, discount\_pct=None, coupon\_fixed=None): subtotal = 0.0 for it in items: subtotal += it\["unit\_price"] \* it\["qty"]
total = subtotal
if discount\_pct:
  total = total - (total \* discount\_pct / 100.0)
if coupon\_fixed:
  total = total - coupon\_fixed
shipping = 25.0 if total < 200.0 else 0.0
total += shipping
vat = total \* 0.15
total += vat
return total # بالريال
def to\_halalas(amount\_sar): return int(amount\_sar \* 100)
def build\_payment\_payload(items, discount\_pct=None, coupon\_fixed=None): total = compute\_order\_total(items, discount\_pct, coupon\_fixed) return { "amount\_halalas": to\_halalas(total), "currency": "SAR", } مثال يُظهر المشكلة عندي على الجهاز: Python items = \[ {"sku": "K-11", "unit\_price": 19.99, "qty": 3}, {"sku": "K-22", "unit\_price": 0.1, "qty": 9}, ] print(build\_payment\_payload(items, discount\_pct=10, coupon\_fixed=5))
{'amount\_halalas': 8285, ...} والواجهة تعرض 82.85 — أحياناً 8284!
المطلوب: تتبّع مصدر الخطأ بدقة، وإصلاحه بحيث تكون كل العمليات الحسابية متسقة ودقيقة إلى الهللة الواحدة في كل الحالات (خصم نسبة + كوبون ثابت + شحن + ضريبة)، مع الحفاظ على توقيع الدوال كما هو حتى لا نكسر الواجهة الأمامية. حلّ واحد مستقل بالمكتبة القياسية فقط، بدون أي تغيير في منطق التسعير ذاته (نسبة الخصم تُطبّق قبل الكوبون، الشحن مجاني فوق 200، ضريبة 15%).
الردودُ الخمسة كما وصلت المحكّمين
أGAIBRA
الخلاصة
الشيفرة تنتج مبلغاً خاطئاً لسببين يعملان معاً: `int()` يقطع ما بعد الهللة ولا يقرّبه، و`float` الثنائيّ يجعل المبلغ أصغر قليلاً من قيمته المكتوبة. مثالك نفسه لا يمكن أن يُخرج 8285 من هذه الشيفرة، فتتبّعي اليدويّ يعطي 8600.
الإصلاح يجري كلّ الحساب بـ`Decimal` ويقرّب مرّةً واحدة إلى الهللة بـ`ROUND\_HALF\_UP`. التوقيعات ومنطق التسعير بقيا كما هما.
تتبّعتُ الشيفرة الأصليّة يدويّاً على سلّتك ولم أشغّلها. المجموع الفرعيّ هو 59.97 زائد 0.9، أي 60.87. خصم 10% يُنزله إلى 54.783، والكوبون يُنزله إلى 49.783. هذا أقلّ من 200 فيُضاف الشحن ويصير 74.783. الضريبة 11.21745، فيكون المجموع 86.00045 ريالاً، ويعطي int(8600.045…) القيمة 8600.
القيمة 8285 لا يمكن أن تخرج من هذه الشيفرة بهذه المدخلات. خطأ float هنا أصغر بكثير من أن ينقل المبلغ هذه المسافة. والدالّة حتميّة، فمدخلٌ واحد لا يعطي 8285 مرّةً و8284 مرّةً أخرى. معنى ذلك أنّ ما رأيته جاء من أحد ثلاثة مصادر: مدخلات غير التي في المثال (كوبون أو خصم آخر)، أو نسخة أخرى من checkout.py في البيئة التي طبعت، أو رقم حسبته الواجهة الأماميّة بطريقتها الخاصّة. لذلك لم أبنِ التشخيص على ذلك الرقم.
العطل الحقيقيّ يظهر في سلّةٍ أخرى بنيتُها لتُطابق رسالة البوابة: منتج واحد بسعر 121.59، وخصم 10، وكوبون 4. القيمة العشريّة الدقيقة لهذه السلّة هي 149.99565 ريالاً. التقريب الصحيح إلى الهللة يعطي 150.00، وهذا ما تعرضه الواجهة. أمّا int(14999.565…) فيقطع الكسر ويرسل 14999. هذا هو الزوج الوارد في رسالتك بالضبط: البوابة سجّلت 14999 منّا، ثمّ وصلها 15000.
الآليّة إذن أنّ القطع يُسقط كلّ كسرٍ أقلّ من هللة، فيخسر الطلب هللةً كلّما كان الكسر نصف هللةٍ أو أكثر. والخصومات المتراكبة تضيف خانات عشريّة جديدة، فيكثر ظهور هذا الكسر، وهذا يفسّر ارتباط العطل بها.
وهناك أثرٌ ثانٍ لـfloat: مبلغٌ صحيح بخانتين قد يُخزَّن أصغر قليلاً من قيمته، فيسقط int(0.29 \* 100) إلى 28. وهذا الأثر نفسه قد يقلب شرط total < 200.0 عند الحدّ، فيُحمَّل الطلب شحناً لا يستحقّه.
أمّا 15001 فلا يمكن أن تنتج عن القطع، لأنّ القطع لا يرفع المبلغ أبداً. فهي تأتي من مسارٍ يقرّب في مواضع غير مواضعنا.
الإصلاح
\# checkout.py
"""حساب مبلغ الطلب بدقّة عشريّة وتقريبٍ واحد إلى الهللة (المكتبة القياسيّة فقط)."""
import math
import unittest
from collections.abc import Iterable
from decimal import ROUND\_HALF\_UP, Decimal, localcontext
from numbers import Integral
HALALA\_QUANTUM = Decimal("0.01")
HALALAS\_PER\_RIYAL = 100
PERCENT\_BASE = Decimal("100")
MAX\_DISCOUNT\_PCT = Decimal("100")
FREE\_SHIPPING\_THRESHOLD\_SAR = Decimal("200")
SHIPPING\_FEE\_SAR = Decimal("25")
VAT\_RATE = Decimal("0.15")
DECIMAL\_PRECISION = 28
CURRENCY\_CODE = "SAR"
UNIT\_PRICE\_KEY = "unit\_price"
QTY\_KEY = "qty"
def \_to\_decimal(value, field\_name):
  """يحوّل رقماً إلى Decimal بقيمته العشريّة المكتوبة (repr) لا بتمثيله الثنائيّ."""
  if isinstance(value, bool):
  raise TypeError(f"{field\_name} must be a number, not bool")
  if isinstance(value, Decimal):
  result = value
  elif isinstance(value, Integral):
  result = Decimal(int(value))
  elif isinstance(value, float):
  if not math.isfinite(value):
  raise ValueError(f"{field\_name} must be finite, got {value!r}")
  result = Decimal(repr(value))
  else:
  raise TypeError(
  f"{field\_name} must be int, float or Decimal, got {type(value).\_\_name\_\_}"
  )
  if not result.is\_finite():
  raise ValueError(f"{field\_name} must be finite, got {value!r}")
  return result
def \_non\_negative\_decimal(value, field\_name):
  """يُرجع Decimal غير سالب أو يرفع ValueError."""
  result = \_to\_decimal(value, field\_name)
  if result < 0:
  raise ValueError(f"{field\_name} must be >= 0, got {value!r}")
  return result
def \_validated\_qty(qty):
  """يتحقّق أنّ الكمّيّة عددٌ صحيح غير سالب."""
  if isinstance(qty, bool) or not isinstance(qty, Integral):
  raise TypeError(f"{QTY\_KEY} must be an integer, got {type(qty).\_\_name\_\_}")
  if qty < 0:
  raise ValueError(f"{QTY\_KEY} must be >= 0, got {qty!r}")
  return int(qty)
def \_validated\_discount\_pct(discount\_pct):
  """None يعني بلا خصم؛ وإلا فالنسبة بين 0 و100."""
  if discount\_pct is None:
  return None
  pct = \_non\_negative\_decimal(discount\_pct, "discount\_pct")
  if pct > MAX\_DISCOUNT\_PCT:
  raise ValueError(f"discount\_pct must be <= {MAX\_DISCOUNT\_PCT}, got {discount\_pct!r}")
  return pct
def \_validated\_coupon(coupon\_fixed):
  """None يعني بلا كوبون؛ وإلا فقيمةٌ ثابتة غير سالبة بالريال."""
  if coupon\_fixed is None:
  return None
  return \_non\_negative\_decimal(coupon\_fixed, "coupon\_fixed")
def \_subtotal(items):
  """مجموع سعر الوحدة × الكمّيّة لكلّ بند، بحساب عشريّ."""
  if isinstance(items, (str, bytes)) or not isinstance(items, Iterable):
  raise TypeError("items must be an iterable of item mappings")
  subtotal = Decimal(0)
  for item in items:
  unit\_price = \_non\_negative\_decimal(item\[UNIT\_PRICE\_KEY], UNIT\_PRICE\_KEY)
  subtotal += unit\_price \* \_validated\_qty(item\[QTY\_KEY])
  return subtotal
def \_order\_total\_exact(items, discount\_pct, coupon\_fixed):
  """منطق التسعير الأصليّ بترتيبه نفسه، بلا أيّ تقريب وسيط."""
  pct = \_validated\_discount\_pct(discount\_pct)
  coupon = \_validated\_coupon(coupon\_fixed)
  with localcontext() as ctx:
  ctx.prec = DECIMAL\_PRECISION
  ctx.rounding = ROUND\_HALF\_UP
  total = \_subtotal(items)
  if pct:
  total = total - (total \* pct / PERCENT\_BASE)
  if coupon:
  total = total - coupon
  shipping = SHIPPING\_FEE\_SAR if total < FREE\_SHIPPING\_THRESHOLD\_SAR else Decimal(0)
  total += shipping
  vat = total \* VAT\_RATE
  return total + vat
def \_sar\_to\_halalas(amount\_sar):
  """تقريبٌ واحد إلى الهللة (نصفٌ فأعلى يُرفع) ثمّ تحويلٌ إلى عدد صحيح."""
  with localcontext() as ctx:
  ctx.prec = DECIMAL\_PRECISION
  rounded = amount\_sar.quantize(HALALA\_QUANTUM, rounding=ROUND\_HALF\_UP)
  return int(rounded \* HALALAS\_PER\_RIYAL)
def \_order\_total\_halalas(items, discount\_pct, coupon\_fixed):
  """المصدر الوحيد لمبلغ الطلب بالهللة."""
  return \_sar\_to\_halalas(\_order\_total\_exact(items, discount\_pct, coupon\_fixed))
def compute\_order\_total(items, discount\_pct=None, coupon\_fixed=None):
  """يُرجع إجماليّ الطلب بالريال (float) مقرّباً إلى الهللة، للعرض فقط.
  القيمة المعتمدة للدفع هي amount\_halalas من build\_payment\_payload.
  """
  return \_order\_total\_halalas(items, discount\_pct, coupon\_fixed) / HALALAS\_PER\_RIYAL
def to\_halalas(amount\_sar):
  """يحوّل مبلغاً بالريال إلى هللات بتقريب ROUND\_HALF\_UP على قيمته العشريّة المكتوبة."""
  return \_sar\_to\_halalas(\_to\_decimal(amount\_sar, "amount\_sar"))
def build\_payment\_payload(items, discount\_pct=None, coupon\_fixed=None):
  """يبني حمولة الدفع؛ يرفض المبلغ غير الموجب لأنّ البوابة لا تقبله."""
  amount\_halalas = \_order\_total\_halalas(items, discount\_pct, coupon\_fixed)
  if amount\_halalas <= 0:
  raise ValueError(f"payment amount must be positive, got {amount\_halalas} halalas")
  return {"amount\_halalas": amount\_halalas, "currency": CURRENCY\_CODE}
USER\_EXAMPLE\_ITEMS = \[
  {"sku": "K-11", "unit\_price": 19.99, "qty": 3},
  {"sku": "K-22", "unit\_price": 0.1, "qty": 9},
]
TRUNCATION\_CASE\_ITEMS = \[{"sku": "T-1", "unit\_price": 121.59, "qty": 1}]
def \_original\_compute\_order\_total(items, discount\_pct=None, coupon\_fixed=None):
  """نسخة حرفيّة من الأصل لإثبات الانحراف في الاختبار."""
  subtotal = 0.0
  for it in items:
  subtotal += it\["unit\_price"] \* it\["qty"]
  total = subtotal
  if discount\_pct:
  total = total - (total \* discount\_pct / 100.0)
  if coupon\_fixed:
  total = total - coupon\_fixed
  shipping = 25.0 if total < 200.0 else 0.0
  total += shipping
  total += total \* 0.15
  return total
def \_original\_to\_halalas(amount\_sar):
  """نسخة حرفيّة من التحويل الأصليّ (قطع)."""
  return int(amount\_sar \* 100)
class CheckoutTests(unittest.TestCase):
  def test\_user\_example\_is\_8600\_halalas(self):
  self.assertEqual(build\_payment\_payload(USER\_EXAMPLE\_ITEMS, 10, 5)\["amount\_halalas"], 8600)
  self.assertEqual(compute\_order\_total(USER\_EXAMPLE\_ITEMS, 10, 5), 86.0)
  def test\_original\_truncates\_150\_cart\_to\_14999(self):
  original = \_original\_compute\_order\_total(TRUNCATION\_CASE\_ITEMS, 10, 4)
  self.assertEqual(\_original\_to\_halalas(original), 14999)
  def test\_fixed\_150\_cart\_is\_15000(self):
  self.assertEqual(build\_payment\_payload(TRUNCATION\_CASE\_ITEMS, 10, 4)\["amount\_halalas"], 15000)
  self.assertEqual(compute\_order\_total(TRUNCATION\_CASE\_ITEMS, 10, 4), 150.0)
  def test\_free\_shipping\_at\_exactly\_200\_after\_discount\_and\_coupon(self):
  items = \[{"sku": "B-1", "unit\_price": 230, "qty": 1}]
  self.assertEqual(build\_payment\_payload(items, 10, 7)\["amount\_halalas"], 23000)
  def test\_shipping\_charged\_just\_below\_200(self):
  items = \[{"sku": "B-2", "unit\_price": 199.99, "qty": 1}]
  self.assertEqual(build\_payment\_payload(items)\["amount\_halalas"], 25874)
  def test\_to\_halalas\_rounds\_written\_value\_half\_up(self):
  self.assertEqual(\_original\_to\_halalas(0.29), 28)
  self.assertEqual(to\_halalas(0.29), 29)
  self.assertEqual(to\_halalas(2.675), 268)
  self.assertEqual(to\_halalas(0.125), 13)
  def test\_display\_value\_and\_payload\_agree(self):
  cases = \[(USER\_EXAMPLE\_ITEMS, 10, 5), (TRUNCATION\_CASE\_ITEMS, 10, 4)]
  for items, pct, coupon in cases:
  self.assertEqual(
  to\_halalas(compute\_order\_total(items, pct, coupon)),
  build\_payment\_payload(items, pct, coupon)\["amount\_halalas"],
  )
  def test\_currency\_is\_sar(self):
  self.assertEqual(build\_payment\_payload(USER\_EXAMPLE\_ITEMS)\["currency"], CURRENCY\_CODE)
  def test\_rejects\_invalid\_inputs(self):
  with self.assertRaises(ValueError):
  compute\_order\_total(\[{"unit\_price": 1.0, "qty": -1}])
  with self.assertRaises(TypeError):
  compute\_order\_total(\[{"unit\_price": True, "qty": 1}])
  with self.assertRaises(TypeError):
  compute\_order\_total(\[{"unit\_price": 1.0, "qty": 1.5}])
  with self.assertRaises(ValueError):
  compute\_order\_total(USER\_EXAMPLE\_ITEMS, discount\_pct=101)
  with self.assertRaises(ValueError):
  compute\_order\_total(USER\_EXAMPLE\_ITEMS, coupon\_fixed=-1)
  with self.assertRaises(ValueError):
  to\_halalas(float("nan"))
  def test\_non\_positive\_amount\_rejected\_for\_payment(self):
  items = \[{"sku": "C-1", "unit\_price": 10, "qty": 1}]
  self.assertEqual(compute\_order\_total(items, coupon\_fixed=50), -17.25)
  with self.assertRaises(ValueError):
  build\_payment\_payload(items, coupon\_fixed=50)
if \_\_name\_\_ == "\_\_main\_\_":
  unittest.main()
القيم المثبّتة في الاختبارات حسبتُها كلّها بالتتبّع اليدويّ، وأمر python checkout.py يفحصها. التقريب يحدث في موضعٍ واحد هو \_sar\_to\_halalas. وسياق decimal محلّيّ لكلّ خيط (thread-local)، فالشيفرة لا تتأثّر بسياقٍ عامّ قد يغيّره جزءٌ آخر من التطبيق.
هذه فروق السلوك عن الأصل كلّها:
الموضع الأصل بعد الإصلاح
to\_halalas قطع (0.29 تصير 28) ROUND\_HALF\_UP على القيمة المكتوبة (29)
compute\_order\_total قيمة غير مقرّبة (نحو 86.00045) مقرّبة إلى الهللة (86.0)
حدّ الشحن يتأثّر بضجيج float مقارنة عشريّة دقيقة
مدخلات bool أو كمّيّة كسريّة أو سالبة أو NaN تُقبل أو تُحسب بلا معنى TypeError أو ValueError
خصم خارج 0..100 أو كوبون سالب يُطبَّق ValueError
مبلغ دفع صفر أو سالب يُرسَل إلى البوابة ValueError في build\_payment\_payload
بقي في الشيفرة سلوكٌ أصليّ لم أغيّره، لأنّك طلبت ألّا يتغيّر منطق التسعير. الأوّل أنّ الطلب الذي يبلغ 200 تماماً يُعفى من الشحن. والثاني أنّ الكوبون الأكبر من قيمة البضاعة يستهلك جزءاً من الشحن، وما زالت compute\_order\_total تُرجع قيمةً سالبة في هذه الحالة.
شرح السبب
العطل لا يخصّ هذا الملفّ وحده، بل هو خطأٌ في البنية: المال يُحمَل بنوعٍ لا يمثّل الكسور العشريّة تمثيلاً دقيقاً، ثمّ يُحوَّل بقطعٍ بدل التقريب، والواجهة تحسب المبلغ بمسارٍ مستقلّ. والكسر الذي يُسقطه القطع تُنتجه الخصومات المتراكبة نفسها. أمّا round() فزاد النتائج سوءاً لسببين: هو يقرّب إلى أقرب زوجيّ (half-even)، ويعمل على قيمةٍ ثنائيّة مخزّنة أصغر من المكتوبة، فتعطي round(2.675, 2) القيمة 2.67.
مهمّ
ما دامت الواجهة الأماميّة تحسب المبلغ بنفسها فسيبقى فرق الهللة واحتمال 15001 قائمين. اجعلها تعرض amount\_halalas القادم من الخادم مقسوماً على مئة، ولا تُعِد حسابه.
هذا العمل هندسيّ مبنيّ على checkout.py وحده، ولم يصلني شيءٌ من شيفرة الواجهة ولا من سياسة تقريب فواتيرك الضريبيّة. فسؤالي: هل تقرّب الواجهة مرّةً واحدة في النهاية، أم تقرّب مبلغ الخصم والضريبة كلّاً على حدة؟ إن كانت تقرّب كلّ خطوة نقلتُ مواضع التقريب في الخادم لتطابقها، فتختفي حالات 15001. والقرار في ذلك لك.
الوقاية
اجعل amount\_halalas العدد الصحيح الوحيد الذي يعبر الحدّ بين الخادم والواجهة والبوابة. وأضف إلى الاختبارات حالاتٍ يقع فيها الكسر على نصف هللة، وحالاتٍ تقع على حدّ 200 بعد الخصم والكوبون، مثل سلّة 121.59 وسلّة 230 أعلاه. وامنع في مراجعة الشيفرة أيّ float أو int() أو round() يمرّ عليه مبلغٌ ماليّ.
🏃 لم يُشغَّل — السبب: Docker غيرُ متاح: docker: An error occurred trying to start process 'docker' with working directory 'D:\\Sites\\gaibra.ai'. The system cannot find the file specified.
ما لا أعرفه: تصنيفُ السبب الجذريّ لهذا الخطأ بعينه
دClaude Sonnet
إعادة حساب المبلغ يدويًا للتحقق من اختلاف النتيجة.
سأتحقق أولاً بتشغيل الكود الحالي والكود المصحَّح قبل أن أعطيك الخلاصة.
Shared a file, ran 3 commands
أرسلت لك checkout.py المصحَّح بنفس تواقيع الدوال الثلاث ومن المكتبة القياسية فقط، لكنه يغيّر سياسة التقريب، ولذلك يلزم التأكد من سياسة الواجهة وسلة قبل النشر (التفاصيل في النقطة 4).
مصدر الخطأ
float يجمع بقيم تقريبية. مثل 0.1 و19.99 وكل خطوة بعدها (نسبة، كوبون، شحن، ضريبة). ينتج عنها ضجيج بحجم 1e-13، يكفي ليجعل 150.00 تساوي 149.99999999.
int(amount \* 100) هي الأسوأ. لا تقرّب بل تقطع الكسر. فالقيمة 149.99999999 تصبح 14999، وهذا سبب الفلس الناقص.
لماذا زاد round() الأمر سوءاً. round() على float في بايثون تقرّب نصف-لأقرب-زوجي وتعمل على التمثيل الثنائي، فصار round(2.675, 2) == 2.67. وعندما وُضعت في خطوة واحدة فقط بقي الباقي غير متسق.
لماذا السلال متعددة الخصومات أسوأ. كل خصم متراكب يضيف خطوة ضرب وقسمة على float، فيتضاعف الضجيج، ويزيد احتمال أن تقع القيمة على حدّ الهللة.
في اختبار على 100 ألف سلة عشوائية اختلف الكود القديم عن الحساب الدقيق في 50161 حالة، منها 49761 تطابق فيها مع قطع القيمة الدقيقة بدل تقريبها. فالقطع هو العامل الغالب، وهذه النسبة أعلى بكثير من 3% عندكم لأن بياناتي العشوائية مليئة بكسور الهللة.
الإصلاح
كل الحساب بـ Decimal، والمدخلات تمر عبر Decimal(str(x))، فيصبح 19.99 بالضبط.
التقريب نصف-للأعلى إلى الهللة عند كل مبلغ ظاهر للعميل: سطر السلة، مبلغ الخصم، الكوبون، الضريبة.
to\_halalas تقرّب ولا تقطع.
أضفت حارساً إضافياً: الكوبون لا يجعل المبلغ سالباً (هذا تغيير سلوك صغير، احذف سطر max(...) إن لم تريده).
بقي compute\_order\_total يُرجع float حفاظاً على التوقيع. القيمة مطابقة للهللة ولا تفقد الدقة عند التحويل.
ما تحققت منه وما لا
مثالك يعطي 8600 في الكود القديم والجديد معاً، ولم أستطع إعادة إنتاج 8285 أو 8284 بتلك المدخلات. ربما تختلف المدخلات الفعلية أو هناك خطوة لا تظهر في الكود الذي أرسلته.
حالات الحدّ التي جرّبتها تعطي النتائج المتوقعة: 150.00 المركّبة من 0.1+0.2+149.7 تصبح 15000، وكوبون أكبر من السلة لا يعطي مبلغاً سالباً.
مطابقة الـ100 ألف سلة كانت مع مرجع يستخدم نفس سياسة التقريب، فهي تثبت اتساق التنفيذ ولا تثبت أن السياسة تطابق الواجهة أو البوابة.
ما يجب التأكد منه قبل النشر
سياسة التقريب. الكود الجديد يقرّب كل خطوة ثم يجمع. هذا يختلف عن التقريب النهائي فقط في نحو 28% من الحالات التي جرّبتها (بند واحد، خصم 10%، كوبون 5). فتأكد من أن الواجهة وسلة يفعلان الشيء نفسه، وإلا فستنتقل المشكلة من فلس ناقص إلى اختلاف سياسة. إن كانا يقرّبان في النهاية فقط، غيّرت لك الكود بسطرين.
رسالة البوابة: expected=14999 وreceived=15000 عندكم، أي أن البوابة تتوقع مبلغاً أقل بهللة. هذا لا يفسّره القطع في كودنا (القطع يعطي أقل لا أكثر)، ويرجّح أن مصدر مبلغ البوابة محسوب بسياسة تقريب مختلفة. الأفضل أن يُعتمد المبلغ نفسه الذي يخرج من سلة كمصدر وحيد إن كان متاحاً في الطلب.
اختبار من الإنتاج: خذ 20 أو 30 طلباً مرفوضاً فعلاً وقارن مبلغ الكود الجديد بمبلغ البوابة قبل النشر. هذا يحسم النقطتين أعلاه أسرع من أي تحليل نظري.
Checkout
PY
from decimal import Decimal, ROUND\_HALF\_UP, localcontext
\_HALALA = Decimal("0.01")
\_HUNDRED = Decimal(100)
def \_dec(value):
  # str() يمنع أي ضجيج ثنائي: float 19.99 -> "19.99" -> Decimal("19.99") بالضبط
  return value if isinstance(value, Decimal) else Decimal(str(value))
def \_q(amount):
  """تقريب إلى الهللة (منزلتان عشريتان) نصف-للأعلى."""
  return amount.quantize(\_HALALA, rounding=ROUND\_HALF\_UP)
def compute\_order\_total(items, discount\_pct=None, coupon\_fixed=None):
  with localcontext() as ctx:
  ctx.prec = 28
  subtotal = Decimal(0)
  for it in items:
  subtotal += \_q(\_dec(it\["unit\_price"]) \* int(it\["qty"])) # سطر السلة بالهللة
  total = subtotal
  if discount\_pct:
  total -= \_q(total \* \_dec(discount\_pct) / \_HUNDRED) # مبلغ الخصم بالهللة
  if coupon\_fixed:
  total -= \_q(\_dec(coupon\_fixed))
  total = max(total, Decimal(0)) # الكوبون لا يجعل المبلغ سالباً
  shipping = Decimal(25) if total < Decimal(200) else Decimal(0)
  total += shipping
  total += \_q(total \* Decimal("0.15")) # الضريبة بالهللة
  return float(total) # قيمة بالريال متطابقة مع الهللة (للتوافق مع التوقيع)
def to\_halalas(amount\_sar):
  return int((\_dec(amount\_sar) \* 100).to\_integral\_value(rounding=ROUND\_HALF\_UP))
def build\_payment\_payload(items, discount\_pct=None, coupon\_fixed=None):
  total = compute\_order\_total(items, discount\_pct, coupon\_fixed)
  return {"amount\_halalas": to\_halalas(total), "currency": "SAR"}
بChatGPT
المشكلة ليست في float وحده؛ بل في تمثيل المبالغ المالية، وتوقيت التقريب، وطريقة تحويل الريال إلى هللات. الحل الآمن هو إجراء الحسابات المالية باستخدام Decimal من المكتبة القياسية، مع اعتماد الهللة كوحدة نهائية صحيحة، وتطبيق التقريب صراحةً في المراحل التي تتطلبه.
1\. تتبّع مصدر الخطأ
هناك ثلاثة مواضع للمشكلة في الكود الحالي:
استخدام float: الأعداد مثل 19.99 و0.1 لا تُمثّل بدقة تامة في النظام الثنائي. وتتراكم الفروق عند ضرب الأسعار في الكميات واحتساب الخصومات والضريبة.
التحويل باستخدام int(amount\_sar \* 100): الدالة int() لا تقرّب إلى أقرب هللة، بل تحذف الجزء العشري باتجاه الصفر. فإذا كان المبلغ الداخلي 149.999999999 ريال، فقد ينتج 14999 بدلًا من 15000 هللة.
غياب سياسة تقريب موحّدة: لا يحدد الكود هل تُقرّب قيمة كل بند، أم الخصم، أم الضريبة، أم الإجمالي النهائي. وهذا مهم لأن التقريب في مراحل مختلفة قد ينتج إجماليات مختلفة.
أما round() فليس حلًا كافيًا: فهو لا يعالج أصل الخطأ الناتج عن float، ويستخدم افتراضيًا قاعدة التقريب إلى العدد الزوجي عند التعادل، وهي ليست بالضرورة القاعدة المطلوبة في التسعير.
2\. الحل الكامل — ملف واحد، دون مكتبات خارجية
سأستخدم Decimal مع ROUND\_HALF\_UP، وأجعل كل مرحلة مالية تُقرّب إلى هللة واحدة وفق سياسة واضحة:
أسعار البنود تُقرأ كقيم عشرية دقيقة.
يُطبّق خصم النسبة أولًا، ثم يُقرّب المبلغ الناتج إلى هللات.
يُخصم الكوبون الثابت بعد تحويله إلى هللات.
يُحسب الشحن بعد الخصومات: مجاني إذا كان المبلغ المتبقي 200 ريال أو أكثر.
تُحسب الضريبة بنسبة 15% على المبلغ بعد الشحن، ثم تُقرّب إلى هللات.
يُجمع الإجمالي النهائي من مبالغ مقربة إلى هللات، دون تحويل عائم غير دقيق.
from decimal import Decimal, ROUND\_HALF\_UP
CENT = Decimal("0.01")
HUNDRED = Decimal("100")
def \_decimal(value):
  """Convert a numeric value to Decimal without importing float artifacts."""
  return Decimal(str(value))
def \_money(value):
  """Round a SAR amount to the nearest halala, ties away from zero."""
  return \_decimal(value).quantize(CENT, rounding=ROUND\_HALF\_UP)
def compute\_order\_total(items, discount\_pct=None, coupon\_fixed=None):
  subtotal = Decimal("0.00")
  for it in items:
  unit\_price = \_money(it\["unit\_price"])
  qty = int(it\["qty"])
  subtotal += unit\_price \* qty
  # Percentage discount first.
  total = subtotal
  if discount\_pct:
  discount = \_money(total \* \_decimal(discount\_pct) / HUNDRED)
  total = \_money(total - discount)
  # Fixed coupon second.
  if coupon\_fixed:
  coupon = \_money(coupon\_fixed)
  total = \_money(total - coupon)
  # Preserve the original shipping threshold and order of operations.
  shipping = Decimal("25.00") if total < Decimal("200.00") else Decimal("0.00")
  total = \_money(total + shipping)
  # VAT is calculated after shipping.
  vat = \_money(total \* Decimal("0.15"))
  total = \_money(total + vat)
  return total # Decimal in SAR
def to\_halalas(amount\_sar):
  """Convert SAR to integer halalas using explicit monetary rounding."""
  return int((\_money(amount\_sar) \* HUNDRED).to\_integral\_exact())
def build\_payment\_payload(items, discount\_pct=None, coupon\_fixed=None):
  total = compute\_order\_total(items, discount\_pct, coupon\_fixed)
  return {
  "amount\_halalas": to\_halalas(total),
  "currency": "SAR",
  }
3\. اختبار المثال والتحقق من النتيجة
هناك ملاحظة مهمة: الناتج 8285 المذكور في السؤال لا يتوافق حسابيًا مع الكود المعروض. وفق الأسعار والكميات والخصومات نفسها، يجب أن يكون الإجمالي 86.00 ريال، وليس 82.85 ريال.
الحساب خطوة بخطوة
إجمالي المنتجات
60.87 ريال
خصم 10%، بعد التقريب
− 6.09 ريال
بعد الخصم النسبي
54.78 ريال
الكوبون الثابت
− 5.00 ريال
الشحن
\+ 25.00 ريال
ضريبة 15% على 74.78 ريال
\+ 11.22 ريال
الإجمالي النهائي
86.00 ريال
المبلغ المرسل للبوابة
8600 هللة
يمكنك اختبار النتيجة مباشرة:
items = \[
  {"sku": "K-11", "unit\_price": 19.99, "qty": 3},
  {"sku": "K-22", "unit\_price": 0.1, "qty": 9},
]
print(compute\_order\_total(items, discount\_pct=10, coupon\_fixed=5))
\# 86.00
print(build\_payment\_payload(items, discount\_pct=10, coupon\_fixed=5))
\# {'amount\_halalas': 8600, 'currency': 'SAR'}
4\. اختبارات إضافية مهمة قبل النشر
هذه الاختبارات تكشف أخطاء التحويل والتقريب عند حدود الهللة وحد الشحن:
from decimal import Decimal
\# Conversion must round, not truncate.
assert to\_halalas("149.999") == 15000
assert to\_halalas("150.001") == 15000
\# Explicit half-up behavior.
assert to\_halalas("1.005") == 101
assert to\_halalas("1.004") == 100
\# Shipping threshold: before VAT and after discounts.
assert compute\_order\_total(
  \[{"unit\_price": "200.00", "qty": 1}]
) == Decimal("230.00")
assert compute\_order\_total(
  \[{"unit\_price": "199.99", "qty": 1}]
) == Decimal("258.74")
5\. تنبيهات قبل اعتماد الإصلاح
لا تخلط Decimal وfloat في أي عملية مالية. استخدم Decimal(str(value)) عند استقبال قيم رقمية قد تكون float، والأفضل أن تصل الأسعار أصلًا كسلاسل عشرية أو قيم عشرية دقيقة من قاعدة البيانات.
التقريب إلى الهللة بعد كل مرحلة مالية هو سياسة تقريب محددة؛ وقد يختلف عن نظام يقرّب الضريبة على مستوى كل بند أو يقرّب الإجمالي مرة واحدة فقط. يجب مطابقة السياسة مع إعدادات سلة ومتطلبات بوابة الدفع والفاتورة الضريبية.
إذا كانت الواجهة تعرض الإجمالي من حساب منفصل، فلا يكفي إصلاح checkout.py وحده. يجب أن تعتمد الواجهة والطلب المرسل للبوابة على الإجمالي المحسوب نفسه، لا على معادلتين مستقلتين.
Decimal لا يُسلسل مباشرةً إلى JSON بواسطة json.dumps القياسي في Python. الكود أعلاه يتجنب هذه المشكلة في حمولة الدفع بإرسال عدد صحيح من الهللات.
الخلاصة: أصلح مصدر الحساب باستخدام Decimal، وحدّد قاعدة التقريب، وأرسل عددًا صحيحًا من الهللات. لكن قبل النشر، تحقّق أيضًا من سبب اختلاف المثال 8285 عن الناتج الحسابي 8600؛ فقد يشير ذلك إلى اختلاف في الكود الفعلي أو إلى خصم أو رسم إضافي في مسار الطلب.
هـDeepSeek
تبّع المشكلة بدقة
مصدر الخطأ
المشكلة الأساسية هي استخدام float في تمثيل النقود. الأرقام العشرية مثل 0.1 و 19.99 و 0.15 لا تُمثَّل تمثيلاً دقيقاً في IEEE-754، فيتراكم خطأ صغير في كل عملية:
python
>>> 0.1 \* 9
0.9000000000000001
>>> 19.99 \* 3
59.97
>>> 19.99 \* 3 + 0.1 \* 9
60.870000000000005
عند تطبيق الخصم والكوبون والشحن والضريبة، يتراكم الخطأ حتى يظهر فرق هللة كاملة عند الضرب في 100:
python
>>> total = 82.84999999999999
>>> int(total \* 100)
8284 # ← هذا هو 8284 الذي تراه أحياناً
لماذا round() زاد الأمور سوءاً؟
لأن round() في Python تستخدم banker's rounding (تقريب إلى الزوجي):
python
>>> round(0.5) # 0
>>> round(1.5) # 2
>>> round(2.5) # 2 ← ليس 3!
فإذا كان الخطأ العائم يجعل القيمة 82.845 أحياناً و82.855 أحياناً أخرى، فإن round(total, 2) قد تختار اتجاهاً مختلفاً في كل مرة، فيظهر عدم الاتساق بين الواجهة والبوابة.
القاعدة الذهبية: لا تستخدم float للنقود أبداً. استخدم أعداداً صحيحة بالهللة (minor units) طوال سلسلة الحساب.
الإصلاح
الفكرة: نحافظ على نفس توقيع الدوال، لكن نُجري كل الحسابات الداخلية بالهللة (int)، ونجمع القيم المالية من المصدر بأعداد صحيحة.
python
from decimal import Decimal, ROUND\_HALF\_UP, ROUND\_DOWN
\# ---------- أدوات التحويل ----------
def \_to\_halalas(value) -> int:
  """
  يحوّل أي قيمة مالية (float/str/int/Decimal) إلى هللة كعدد صحيح.
  نمر عبر str() لتفادي أخطاء float الأصلية (0.1 → "0.1" لا 0.1000000000000000055).
  """
  if isinstance(value, int):
  return value \* 100
  d = Decimal(str(value))
  # التقريب إلى الهللة: HALF\_UP هو المتعارف عليه تجارياً
  return int(d.quantize(Decimal("0.01"), rounding=ROUND\_HALF\_UP) \* 100)
def \_from\_halalas(h: int) -> float:
  """لإرجاع القيمة بالريال في واجهات العرض فقط، دون استخدامها في الحساب."""
  return float(Decimal(h) / Decimal(100))
\# ---------- منطق التسعير (كما هو، لكن بالهللة) ----------
def \_compute\_order\_total\_halalas(items, discount\_pct=None, coupon\_fixed=None) -> int:
  # 1) المجموع الفرعي: كل صنف يُقرَّب إلى هللة ثم يُضرب في الكمية (مطابق للعرض في السلة)
  subtotal\_h = 0
  for it in items:
  unit\_h = \_to\_halalas(it\["unit\_price"])
  subtotal\_h += unit\_h \* int(it\["qty"])
  total\_h = subtotal\_h
  # 2) خصم النسبة: نطبّق على الإجمالي بالهللة، ونقرّب الناتج إلى هللة واحدة
  if discount\_pct:
  # total \* pct / 100 بالهللة، بتقريب HALF\_UP
  discount\_h = int(
  (Decimal(total\_h) \* Decimal(str(discount\_pct)) / Decimal(100))
  .quantize(Decimal("1"), rounding=ROUND\_HALF\_UP)
  )
  total\_h -= discount\_h
  # 3) الكوبون الثابت
  if coupon\_fixed:
  coupon\_h = \_to\_halalas(coupon\_fixed)
  total\_h -= coupon\_h
  # 4) الشحن: مجاني إذا كان الإجمالي (بعد الخصومات) ≥ 200 ريال
  shipping\_h = 0 if total\_h >= 20000 else 2500 # 25.00 ريال = 2500 هللة
  total\_h += shipping\_h
  # 5) الضريبة 15% على (الإجمالي + الشحن)
  vat\_h = int(
  (Decimal(total\_h) \* Decimal("0.15"))
  .quantize(Decimal("1"), rounding=ROUND\_HALF\_UP)
  )
  total\_h += vat\_h
  return total\_h
\# ---------- الدوال العامة بنفس التوقيع ----------
def compute\_order\_total(items, discount\_pct=None, coupon\_fixed=None):
  """تُرجع الإجمالي بالريال (float) للتوافق مع الواجهة الحالية فقط."""
  return \_from\_halalas(
  \_compute\_order\_total\_halalas(items, discount\_pct, coupon\_fixed)
  )
def to\_halalas(amount\_sar) -> int:
  """يحوّل مبلغاً بالريال إلى هللة بعدد صحيح."""
  return \_to\_halalas(amount\_sar)
def build\_payment\_payload(items, discount\_pct=None, coupon\_fixed=None):
  total\_h = \_compute\_order\_total\_halalas(items, discount\_pct, coupon\_fixed)
  return {
  "amount\_halalas": total\_h,
  "currency": "SAR",
  }
التحقق على مثالك
python
items = \[
  {"sku": "K-11", "unit\_price": 19.99, "qty": 3},
  {"sku": "K-22", "unit\_price": 0.1, "qty": 9},
]
print(build\_payment\_payload(items, discount\_pct=10, coupon\_fixed=5))
حساب يدوي بالهللة:
الخطوة القيمة (هللة)
19.99 × 3 = 59.97 5997
0.10 × 9 = 0.90 90
المجموع الفرعي 6087
خصم 10% = 608.7 → HALF\_UP 609
بعد الخصم 5478
كوبون ثابت 5.00 −500
بعد الكوبون 4978
شحن (4978 < 20000) +2500
قبل الضريبة 7478
ضريبة 15% = 1121.7 → HALF\_UP 1122
الإجمالي النهائي 8600 هللة = 86.00 ريال
النتيجة ثابتة دائماً: {'amount\_halalas': 8600, 'currency': 'SAR'}، ولا يوجد أي احتمال لتذبذب بين 8599 و8601.
نقاط مهمة لتفادي المشكلة مستقبلاً
قاعدة صارمة: أي متغير يمثل نقوداً يجب أن يكون int بالهللة أو Decimal، وليس float. طبّق هذه القاعدة على قاعدة البيانات أيضاً (خزّن unit\_price\_halalas كـ INTEGER بدل unit\_price كـ FLOAT).
التقريب موحّد: استخدم ROUND\_HALF\_UP (المتعارف عليه تجارياً في السعودية وأغلب بوابات الدفع)، لا round() الافتراضية. وثّق سياسة التقريب في مكان واحد وطبّقها في كل الأماكن (السلة، الفاتورة، بوابة الدفع).
ترتيب العمليات ثابت: خصم النسبة → الكوبون → الشحن → الضريبة. حتى لو تغيّر شيء لاحقاً، لا تُغيّر الترتيب دون مواءمة مع بوابة الدفع والفوترة الإلكترونية (ZATCA تتطلب تسلسلاً محدداً).
تحقق نهائي قبل الإرسال: أضف حاجزاً أخيراً يرفض الإرسال إذا لم يُطابق المبلغ المُرسَل ما تعرضه الواجهة:
python
payload = build\_payment\_payload(items, discount\_pct, coupon\_fixed)
assert payload\["amount\_halalas"] == frontend\_total\_halalas, "mismatch"
لا تُرسل float أبداً للبوابة: أرسل دائماً الحقل الصحيح (amount\_halalas كـ int). بعض البوابات تقبل amount كـ float فتُعيد نفس المشكلة من طرفها.
الاختبارات: أضف اختبارات حدودية (property-based إن أمكن) بقيم مثل 0.01, 0.10, 19.99, 99.995، وسلال بخصومات متراكبة، للتأكد من أن amount\_halalas الناتج ثابت لا يتغير بين تشغيلين.
جGemini
السبب الرئيسي للمشكلة هو عدم دقة الأرقام ذات الفاصلة العائمة (float) عند تمثيل أعداد مثل 0.1 أو 19.99 في ذاكرة الحاسوب binary system، بالإضافة إلى استخدام int(...) للتقريب الذي يقطع الجزء العشري دائماً (Floor truncation) بدلاً من التقريب المالي الصحيح.
1\. تحليل المشكلة بدقة
المشكلة الأولى: عدم دقة الفاصلة العائمة (float)
عند تنفيذ الحسابات بالـ float:
الرقم 19.99 يُمثَّل داخلياً تقريباً بـ 19.989999999999998436...
النتيجة النهائية لـ compute\_order\_total قد تخرج مثلاً: 82.84999999999999
عندما تقوم بتحويلها إلى هللات: 82.84999999999999 \* 100 تصبح 8284.999999999999
استخدام int(...) يقطع الأعشار دون تقريب، فتتحول النتيجة إلى 8284 هللة بدلاً من 8285.
المشكلة الثانية: تقريب منتصف المسافة (Midpoint Rounding)
استخدام round() العادية في بايثون يتبع معيار ROUND\_HALF\_EVEN (التقريب للرقم الزوجي الأقرب)، وهو ما يتسبب بفرق هللة في بعض الحالات المالية التي تعتمد تقريب المنتصف للأعلى ROUND\_HALF\_UP (أو تقريب السلة النهائي بشكل موحد).
2\. الحل الجذرِي باستخدام decimal.Decimal
لحل المشكلة بشكل كامل وتفادي أي فروقات في الهللات، يُوصى باستخدام وحدة Decimal من المكتبة القياسية لبايثون مع ضبط طريقة التقريب المالية ROUND\_HALF\_UP.
كود checkout.py المعدل:
Python
from decimal import Decimal, ROUND\_HALF\_UP
def compute\_order\_total(items, discount\_pct=None, coupon\_fixed=None):
  # تحويل القيم الأساسية إلى Decimal بدقة
  subtotal = Decimal("0.0")
  for it in items:
  unit\_price = Decimal(str(it\["unit\_price"]))
  qty = Decimal(str(it\["qty"]))
  subtotal += unit\_price \* qty
  total = subtotal
 
  # تطبيق الخصم المئوي
  if discount\_pct:
  discount = Decimal(str(discount\_pct)) / Decimal("100.0")
  total = total - (total \* discount)
 
  # تطبيق الكوبون الثابت
  if coupon\_fixed:
  total = total - Decimal(str(coupon\_fixed))
  # احتساب الشحن (الشحن مجاني إذا كان الإجمالي بعد الخصومات >= 200)
  shipping = Decimal("25.0") if total < Decimal("200.0") else Decimal("0.0")
  total += shipping
  # احتساب ضريبة القيمة المضافة 15%
  vat = total \* Decimal("0.15")
  total += vat
  # تقريب النتيجة النهائية إلى أقرب هللة (جزءين عشريين)
  total\_sar = total.quantize(Decimal("0.01"), rounding=ROUND\_HALF\_UP)
 
  # إعادة float لضمان عدم كسر توقيع الدالة أو التوافق مع الواجهة الأمامية
  return float(total\_sar)
def to\_halalas(amount\_sar):
  # تحويل الـ float إلى Decimal ثم التقريب المباشر للهللات بأمان
  amount\_dec = Decimal(str(amount\_sar))
  halalas = (amount\_dec \* Decimal("100")).quantize(Decimal("1"), rounding=ROUND\_HALF\_UP)
  return int(halalas)
def build\_payment\_payload(items, discount\_pct=None, coupon\_fixed=None):
  total = compute\_order\_total(items, discount\_pct, coupon\_fixed)
  return {
  "amount\_halalas": to\_halalas(total),
  "currency": "SAR",
  }
3\. اختبار الحل على المثال الخاص بك
Python
items = \[
  {"sku": "K-11", "unit\_price": 19.99, "qty": 3},
  {"sku": "K-22", "unit\_price": 0.1, "qty": 9},
]
\# النتيجة:
payload = build\_payment\_payload(items, discount\_pct=10, coupon\_fixed=5)
print(payload)
\# Output: {'amount\_halalas': 8285, 'currency': 'SAR'}
ملخص التحسينات:
دقة الحسابات: جميع العمليات الرياضية (الخصم، الشحن، الضريبة) تم نقلها لتستخدم Decimal لمنع تراكم أخطاء الفاصلة العائمة.
التقريب التجاري Standard Financial Rounding: استخدام ROUND\_HALF\_UP يضمن أن .005 تقرب دائماً للأعلى إلى .01 بشكل مطابق تماماً لما يعرضه المتجر للعميل.
التوافق التام: استرجاع float من compute\_order\_total يضمن عدم كسر أي جزء آخر في التطبيق يعتمد على توقيع الدالة الحالي.