• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

stripe / stripe-mirakl-connector / 35328222280

18 Sep 2026 09:11AM UTC coverage: 95.974% (-0.2%) from 96.223%
35328222280

push

github

web-flow
Merge pull request #152 from stripe/fix/C-3350-3351-payment-mapping-bug-bounty

Improve payment mapping security

90 of 99 new or added lines in 3 files covered. (90.91%)

2217 of 2310 relevant lines covered (95.97%)

10.81 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

89.77
/src/Repository/PaymentMappingRepository.php
1
<?php
2

3
namespace App\Repository;
4

5
use App\Entity\PaymentMapping;
6
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
7
use Doctrine\Persistence\ManagerRegistry;
8
use Psr\Log\LoggerAwareInterface;
9
use Psr\Log\LoggerAwareTrait;
10

11
/**
12
 * @method PaymentMapping|null find($id, $lockMode = null, $lockVersion = null)
13
 * @method PaymentMapping|null findOneBy(array $criteria, array $orderBy = null)
14
 * @method PaymentMapping[]    findAll()
15
 * @method PaymentMapping[]    findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
16
 */
17
class PaymentMappingRepository extends ServiceEntityRepository implements LoggerAwareInterface
18
{
19
    use LoggerAwareTrait;
20

21
    /**
22
     * PaymentMappingRepository constructor.
23
     */
24
    public function __construct(ManagerRegistry $registry)
176 ✔
25
    {
26
        parent::__construct($registry, PaymentMapping::class);
176 ✔
27
    }
28

29
    public function persist(PaymentMapping $paymentMapping): PaymentMapping
49 ✔
30
    {
31
        $this->getEntityManager()->persist($paymentMapping);
49 ✔
32

33
        return $paymentMapping;
49 ✔
34
    }
35

36
    public function flush(): void
49 ✔
37
    {
38
        $this->getEntityManager()->flush();
49 ✔
39
    }
40

41
    /**
42
     * Persists a mapping when no mapping exists for its commercial order.
43
     *
44
     * Returns the existing mapping when the commercial order is already mapped;
45
     * otherwise returns null after creating the supplied mapping.
46
     */
47
    public function persistIfCommercialOrderIsUnmapped(PaymentMapping $paymentMapping): ?PaymentMapping
12 ✔
48
    {
49
        $commercialOrderId = $paymentMapping->getMiraklCommercialOrderId();
12 ✔
50
        if (null === $commercialOrderId) {
12 ✔
51
            throw new \InvalidArgumentException('A commercial order ID is required to create a payment mapping.');
1 ✔
52
        }
53

54
        $connection = $this->getEntityManager()->getConnection();
11 ✔
55
        $connection->beginTransaction();
11 ✔
56

57
        try {
58
            if ($connection->getDatabasePlatform() instanceof \Doctrine\DBAL\Platforms\PostgreSQLPlatform) {
11 ✔
NEW
59
                $connection->executeStatement(
×
NEW
60
                    'SELECT pg_advisory_xact_lock(hashtextextended(:id, 0))',
×
NEW
61
                    ['id' => $commercialOrderId]
×
NEW
62
                );
×
63
            }
64

65
            $existingMapping = $this->findOneByMiraklCommercialOrderId($commercialOrderId);
11 ✔
66
            if (null !== $existingMapping) {
11 ✔
67
                $connection->rollBack();
2 ✔
68

69
                return $existingMapping;
2 ✔
70
            }
71

72
            $this->getEntityManager()->persist($paymentMapping);
9 ✔
73
            $this->getEntityManager()->flush();
9 ✔
74
            $connection->commit();
9 ✔
75

76
            return null;
9 ✔
NEW
77
        } catch (\Throwable $exception) {
×
NEW
78
            if ($connection->isTransactionActive()) {
×
NEW
79
                $connection->rollBack();
×
80
            }
81

NEW
82
            throw $exception;
×
83
        }
84
    }
85

86
    private function mapByMiraklCommercialOrderId(array $paymentMappings): array
90 ✔
87
    {
88
        if (empty($paymentMappings)) {
90 ✔
89
            return [];
72 ✔
90
        }
91

92
        // Collect the commercial order IDs visible in the (possibly status-filtered) input.
93
        $commercialOrderIds = array_values(array_unique(array_filter(
32 ✔
94
            array_map(static fn($pm) => $pm->getMiraklCommercialOrderId(), $paymentMappings)
32 ✔
95
        )));
32 ✔
96

97
        if (empty($commercialOrderIds)) {
32 ✔
NEW
98
            return [];
×
99
        }
100

101
        // Query ALL rows for those commercial order IDs, ignoring any status filter the
102
        // caller applied. A status-filtered query (e.g. findToCapturePayments) might show
103
        // only one of two duplicate rows; the hidden duplicate would not be detected unless
104
        // we count across every status here.
105
        $conflicted = [];
32 ✔
106
        $countPerOrder = [];
32 ✔
107
        foreach ($this->findBy(['miraklCommercialOrderId' => $commercialOrderIds]) as $row) {
32 ✔
108
            $id = $row->getMiraklCommercialOrderId();
32 ✔
109
            $countPerOrder[$id] = ($countPerOrder[$id] ?? 0) + 1;
32 ✔
110
            if ($countPerOrder[$id] > 1) {
32 ✔
111
                $conflicted[$id] = true;
12 ✔
112
            }
113
        }
114

115
        // Log an error for every conflicted order so operators can investigate.
116
        // Affected orders are intentionally excluded from the result (see below),
117
        // which means they will not be captured until the duplicate rows are resolved.
118
        if (!empty($conflicted)) {
32 ✔
119
            $details = [];
12 ✔
120
            foreach ($conflicted as $orderId => $_) {
12 ✔
121
                $details[] = sprintf('%s (%d rows)', $orderId, $countPerOrder[$orderId]);
12 ✔
122
            }
123
            $this->logger?->error(
12 ✔
124
                sprintf(
12 ✔
125
                    'Duplicate PaymentMapping rows detected for %d commercial order(s) — those orders are excluded from the capture queue until the duplicates are resolved. Affected: %s',
12 ✔
126
                    count($conflicted),
12 ✔
127
                    implode(', ', $details)
12 ✔
128
                ),
12 ✔
129
                ['conflicted_order_ids' => array_keys($conflicted)]
12 ✔
130
            );
12 ✔
131
        }
132

133
        // Build the result map, excluding any commercial order that has duplicate rows in
134
        // any status. Exclusion is safer than guessing which row is legitimate.
135
        $map = [];
32 ✔
136
        foreach ($paymentMappings as $paymentMapping) {
32 ✔
137
            $commercialId = $paymentMapping->getMiraklCommercialOrderId();
32 ✔
138
            if (!isset($conflicted[$commercialId])) {
32 ✔
139
                $map[$commercialId] = $paymentMapping;
22 ✔
140
            }
141
        }
142

143
        return $map;
32 ✔
144
    }
145

146
    /**
147
     * @return PaymentMapping[]
148
     */
149
    public function findToCapturePayments(): array
24 ✔
150
    {
151
        return $this->mapByMiraklCommercialOrderId($this->findBy([
24 ✔
152
            'status' => [
24 ✔
153
                PaymentMapping::TO_CAPTURE,
24 ✔
154
                PaymentMapping::CAPTURE_FAILED,
24 ✔
155
                PaymentMapping::CANCEL_FAILED,
24 ✔
156
            ],
24 ✔
157
        ]));
24 ✔
158
    }
159

160
    /**
161
     * @return PaymentMapping[]
162
     */
163
    public function findPaymentsByCommercialOrderIds(array $commercialOrderIds): array
63 ✔
164
    {
165
        return $this->mapByMiraklCommercialOrderId($this->findBy([
63 ✔
166
            'miraklCommercialOrderId' => $commercialOrderIds,
63 ✔
167
        ]));
63 ✔
168
    }
169

170
    /**
171
     * @return PaymentMapping[]
172
     */
173
    public function findPaymentsByCommercialOrderIdsAndStatuses(array $commercialOrderIds, array $status): array
17 ✔
174
    {
175
        return $this->mapByMiraklCommercialOrderId($this->findBy([
17 ✔
176
            'miraklCommercialOrderId' => $commercialOrderIds,
17 ✔
177
            'status' => $status,
17 ✔
178
        ]));
17 ✔
179
    }
180

181
    public function findOneByStripeChargeId(string $stripeChargeId): ?PaymentMapping
23 ✔
182
    {
183
        return $this->findOneBy([
23 ✔
184
            'stripeChargeId' => $stripeChargeId,
23 ✔
185
        ]);
23 ✔
186
    }
187

188
    public function findOneByMiraklCommercialOrderId(string $commercialOrderId): ?PaymentMapping
14 ✔
189
    {
190
        return $this->findOneBy([
14 ✔
191
            'miraklCommercialOrderId' => $commercialOrderId,
14 ✔
192
        ]);
14 ✔
193
    }
194
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc