not->toBeFalse(); $start = strpos($content, "\$this->post('/orders/module/stripe/payment_intent'"); $end = strpos($content, "\$this->post('/orders/module/stripe/debug/simulate_payment'"); expect($start)->not->toBeFalse(); expect($end)->not->toBeFalse(); $stripeSection = substr($content, (int)$start, (int)$end - (int)$start); expect($stripeSection)->toContain('buildStripePaymentIntentResponse('); expect($stripeSection)->toContain('$tax_percentage = self::CARD_PAYMENT_TAX_PERCENTAGE;'); expect($stripeSection)->toContain('$expectedPaymentIntentAmount = $this->getStripePaymentIntentAmountForOrder($order, $tax_percentage);'); expect($stripeSection)->toContain('isStripePaymentIntentReusable($storedPaymentIntent)'); expect($stripeSection)->toContain('doesStripePaymentIntentMatchOrder($storedPaymentIntent, $expectedPaymentIntentAmount, $tax_percentage)'); expect($stripeSection)->toContain("\$storedPaymentIntentStatus = strtolower((string)(\$storedPaymentIntent->status ?? ''));"); expect($stripeSection)->toContain("if (\$storedPaymentIntentStatus === 'succeeded')"); expect($stripeSection)->toContain("'code' => 'stripe_payment_reconciliation_conflict'"); expect($stripeSection)->toContain('$this->recordSucceededStripePayment($order, $storedPaymentIntent);'); expect($stripeSection)->toContain("'already_succeeded' => true"); expect($stripeSection)->toContain('if (!$stripePaymentIntents->delete())'); expect($stripeSection)->toContain('$stripePaymentIntents->tax_percentage->set($tax_percentage);'); expect($stripeSection)->toContain('[$order, $orderPaymentLock] = $this->acquireStripePaymentLocks($order);'); expect(substr_count($stripeSection, 'if (!$stripePaymentIntents->delete())'))->toBeGreaterThanOrEqual(3); $succeededPosition = strpos($stripeSection, "if (\$storedPaymentIntentStatus === 'succeeded')"); $matchPosition = strpos( $stripeSection, 'doesStripePaymentIntentMatchOrder($storedPaymentIntent, $expectedPaymentIntentAmount, $tax_percentage)' ); $deletePosition = strpos($stripeSection, 'if (!$stripePaymentIntents->delete())'); $readerUpdatePosition = strpos($stripeSection, '$stripePaymentIntents->setReaderId($readerId);'); expect($succeededPosition)->not->toBeFalse(); expect($matchPosition)->not->toBeFalse(); expect($deletePosition)->not->toBeFalse(); expect($readerUpdatePosition)->not->toBeFalse(); expect($succeededPosition)->toBeLessThan($matchPosition); expect($readerUpdatePosition)->toBeGreaterThan($matchPosition); expect($deletePosition)->toBeGreaterThan($matchPosition); expect($stripeSection)->toContain('$stripe->payment_intents->create( $expectedPaymentIntentAmount,'); expect($stripeSection)->toContain("'reused' => true"); expect($stripeSection)->toContain("'message' => 'No active payment intent for this order.'"); expect($stripeSection)->toContain("'message' => 'Payment intent cleared successfully.'"); expect($stripeSection)->toContain("Stored payment intent is stale. Start the payment again."); expect($stripeSection)->toContain("'already_succeeded' => true"); expect($stripeSection)->toContain("Payment intent was cancelled. Start the payment again."); expect($stripeSection)->toContain("Payment intent is not ready to capture."); expect($stripeSection)->toContain('captureApprovedStripePaymentIntent('); expect(substr_count($stripeSection, 'captureApprovedStripePaymentIntent('))->toBeGreaterThanOrEqual(4); expect($stripeSection)->not->toContain("Order does not have a payment intent"); expect($content)->toContain('private const CARD_PAYMENT_TAX_PERCENTAGE = 25;'); expect($content)->toContain('private function doesStripePaymentIntentMatchOrder(object $paymentIntent, int $expectedAmount, ?int $tax_percentage): bool'); expect($content)->toContain('private function captureApprovedStripePaymentIntent('); expect($content)->toContain('order_payment_lock $orderPaymentLock'); expect($content)->toContain('private function acquireStripePaymentLocks(orders_o $order): array'); expect($content)->toContain('order_payment_lock::tryAcquireOrderMutation((int)$order->id)'); expect($content)->toContain("'code' => 'stripe_payment_collection_missing'"); expect($content)->toContain("'code' => 'order_payment_locked'"); expect($content)->toContain("'code' => 'stripe_payment_intent_contract_mismatch'"); expect($content)->toContain("'code' => 'stripe_payment_reconciliation_conflict'"); expect($content)->toContain('$expectedAmount = $this->getStripePaymentIntentAmountForOrder('); $captureHelperStart = strpos($content, 'private function captureApprovedStripePaymentIntent('); $captureHelperEnd = strpos($content, 'private function buildStripePaymentIntentResponse(', (int)$captureHelperStart); expect($captureHelperStart)->not->toBeFalse(); expect($captureHelperEnd)->not->toBeFalse(); $captureHelper = substr($content, (int)$captureHelperStart, (int)$captureHelperEnd - (int)$captureHelperStart); $contractValidationPosition = strpos($captureHelper, 'doesStripePaymentIntentMatchOrder('); $stripeCapturePosition = strpos($captureHelper, '$stripe->payment_intents->capture('); expect($contractValidationPosition)->not->toBeFalse(); expect($stripeCapturePosition)->not->toBeFalse(); expect($contractValidationPosition)->toBeLessThan($stripeCapturePosition); $collectionValidationPosition = strpos($captureHelper, '$this->requireStripePaymentCollectionAvailable($order);'); expect($collectionValidationPosition)->not->toBeFalse(); expect($collectionValidationPosition)->toBeLessThan($stripeCapturePosition); expect($content)->toContain('private function recordSucceededStripePayment(orders_o $order, object $paymentIntent): void'); expect($content)->toContain('$isExactRecordedPayment = ('); expect($content)->toContain('$isCompatiblePartialPayment = ('); expect($content)->toContain('if ($isCompatiblePartialPayment)'); expect($content)->toContain('$orderCollection->paidWithStripe((string)$paymentIntent->id);'); expect($content)->toContain('!isset($paymentIntent->amount) || (int)$paymentIntent->amount !== $expectedAmount'); expect($content)->toContain('!isset($paymentIntent->amount_received) || (int)$paymentIntent->amount_received !== $expectedAmount'); expect($captureHelper)->toContain('$expectedAmountAfterCapture = $this->getStripePaymentIntentAmountForOrder('); expect($content)->toContain("\$storedTaxPercentage = \$metadata['tax_percentage'] ?? null;"); expect($content)->toContain('return (int)$storedTaxPercentage === ($tax_percentage ?? self::CARD_PAYMENT_TAX_PERCENTAGE);'); $updateOrderStart = strpos($content, '#[NoReturn] private function updateOrder(): void'); expect($updateOrderStart)->not->toBeFalse(); $updateOrderSection = substr($content, (int)$updateOrderStart); expect($updateOrderSection)->toContain( '$orderPaymentLock = $this->acquireOrderPaymentLock((int)$order->id);' ); }); it('serializes order price mutations with card payment capture', function (): void { $routeFile = dirname(__DIR__, 3) . '/routes/orderItemsRoute.php'; $lockFile = dirname(__DIR__, 3) . '/classes/order_payment_lock.php'; $routeContent = file_get_contents($routeFile); $lockContent = file_get_contents($lockFile); expect($routeContent)->not->toBeFalse() ->and($lockContent)->not->toBeFalse(); expect($routeContent)->toContain('use classes\order_payment_lock;'); expect(substr_count($routeContent, '$this->acquireOrderPaymentLock('))->toBe(3); expect($routeContent)->toContain("'code' => 'order_payment_locked'"); expect($lockContent)->toContain('public static function tryAcquire(int $orderId): ?self'); expect($lockContent)->toContain('public static function tryAcquireInvoiceCollection(int $invoiceCollectionId): ?self'); expect($lockContent)->toContain('catch (UnexpectedValueException)'); expect($lockContent)->toContain("SELECT GET_LOCK(?, ?) AS acquired"); expect($lockContent)->toContain("SELECT RELEASE_LOCK(?)"); expect($lockContent)->toContain('public function __destruct()'); $ordersObjectFile = dirname(__DIR__, 3) . '/objects/orders_o.php'; $orderItemsObjectFile = dirname(__DIR__, 3) . '/objects/order_items_o.php'; $collectionsObjectFile = dirname(__DIR__, 3) . '/objects/collected_order_invoices_o.php'; $bulkActionFile = dirname(__DIR__, 3) . '/classes/invoice_collection_bulk_action_service.php'; $orderInvoicesRouteFile = dirname(__DIR__, 3) . '/routes/orderInvoicesRoute.php'; $ordersObjectContent = file_get_contents($ordersObjectFile); $orderItemsObjectContent = file_get_contents($orderItemsObjectFile); $collectionsObjectContent = file_get_contents($collectionsObjectFile); $bulkActionContent = file_get_contents($bulkActionFile); $orderInvoicesRouteContent = file_get_contents($orderInvoicesRouteFile); expect($ordersObjectContent)->not->toBeFalse() ->and($orderItemsObjectContent)->not->toBeFalse() ->and($collectionsObjectContent)->not->toBeFalse() ->and($bulkActionContent)->not->toBeFalse() ->and($orderInvoicesRouteContent)->not->toBeFalse(); $assignmentStart = strpos($ordersObjectContent, 'public function assignToInvoiceCollection('); expect($assignmentStart)->not->toBeFalse(); $assignmentSection = substr($ordersObjectContent, (int)$assignmentStart, 2400); expect($assignmentSection)->toContain('order_payment_lock::tryAcquireReassignment('); expect($assignmentSection)->toContain("'code' => 'order_payment_locked'"); expect($lockContent)->toContain('public static function tryAcquireOrderMutations(array $orderIds): ?self'); expect($lockContent)->toContain('public static function tryAcquireReassignment('); expect($lockContent)->toContain('public static function tryAcquireInvoiceCollections('); expect($lockContent)->toContain('public static function tryAcquireInvoiceCollectionWithOrders('); expect($orderItemsObjectContent)->toContain( 'order_payment_lock::tryAcquireOrderMutations($orderIds)' ); expect($collectionsObjectContent)->toContain( 'order_payment_lock::tryAcquireInvoiceCollectionWithOrders((int)$this->id)' ); expect($bulkActionContent)->toContain( 'order_payment_lock::tryAcquireInvoiceCollections(' ); expect($orderInvoicesRouteContent)->not->toContain( '$order->customer_id->set((int)$new_invoice_collection->customer_number->value());' ); expect($collectionsObjectContent)->toContain( '$order_object->assignToInvoiceCollection((int)$tmp->id);' ); }); it('documents fixed card payment moms in the OpenAPI request contract', function (): void { $openApiFiles = array_values(array_filter([ dirname(__DIR__, 6) . '/openapi.yaml', dirname(__DIR__, 3) . '/openapi.yaml', dirname(__DIR__, 6) . '/documentation/generated/openapi.json', ], static fn (string $path): bool => file_exists($path))); expect($openApiFiles)->not->toBeEmpty(); foreach ($openApiFiles as $openApiFile) { $content = file_get_contents($openApiFile); expect($content)->not->toBeFalse(); $isJson = str_ends_with($openApiFile, '.json'); $start = strpos( $content, $isJson ? '"/orders/module/stripe/payment_intent":' : '/orders/module/stripe/payment_intent:' ); $end = strpos( $content, $isJson ? '"/orders/module/stripe/payment_intent/capture":' : ' /orders/module/stripe/payment_intent/capture:', (int)$start ); expect($start)->not->toBeFalse(); expect($end)->not->toBeFalse(); $paymentIntentSection = substr($content, (int)$start, (int)$end - (int)$start); expect($paymentIntentSection)->toContain('fixed 25% moms'); expect($paymentIntentSection)->not->toContain('tax_percentage'); } });