public function processMessage($message) { $logIdentifier = "message with gateway {$message['gateway']}" . " and order ID {$message['order_id']}"; if ($this->paymentsInitialDatabase->isTransactionFailed($message)) { // Throw the message out if it's already failed Logger::info("Skipping failed {$logIdentifier}"); } else { Logger::info("Storing {$logIdentifier} in database"); $this->pendingDatabase->storeMessage($message); } }
/** * We refuse to consume a message and drop it if the corresponding * payments_initial row is failed. */ public function testPendingMessageInitialFailed() { $initRow = PaymentsInitialDatabaseTest::generateTestMessage(); $initRow['payments_final_status'] = 'failed'; $initRow['validation_action'] = 'reject'; $this->paymentsInitialDb->storeMessage($initRow); $message = self::generatePendingMessageFromInitial($initRow); $consumer = new PendingQueueConsumer('pending', 1000, 1000); $consumer->processMessage($message); $fetched = $this->pendingDb->fetchMessageByGatewayOrderId($message['gateway'], $message['order_id']); $this->assertNull($fetched, 'Message consumed and not stored in the pending database.'); }
public function execute() { $this->logger = Logger::getTaggedLogger("corr_id-{$this->correlationId}"); $this->logger->info("Running capture request job on account '{$this->account}' with reference '{$this->pspReference}' " . "and correlation id '{$this->correlationId}'."); // Determine if a message exists in the pending database; if it does not then // this payment has already been sent to the verified queue, or there is a // problem with the database. If it does exist, we need to check // $capture_requested in case we have requested a capture but have not yet // received notification of capture success. Either case can occur when a // donor submits their credit card details multiple times against a single // order ID. We should cancel duplicate authorizations, but leave payments // with missing donor details open for potential manual capture. $this->logger->debug('Attempting to locate associated message in pending database.'); $db = PendingDatabase::get(); $dbMessage = $db->fetchMessageByGatewayOrderId('adyen', $this->merchantReference); $success = true; $action = $this->determineAction($dbMessage); switch ($action) { case self::ACTION_PROCESS: // Attempt to capture the payment /** * @var AdyenPaymentsInterface */ $api = $this->getApi(); $this->logger->info("Attempting capture API call for currency '{$this->currency}', " . "amount '{$this->amount}', reference '{$this->pspReference}'."); $captureResult = $api->capture($this->currency, $this->amount, $this->pspReference); if ($captureResult) { // Success! $this->logger->info("Successfully captured payment! Returned reference: '{$captureResult}'. " . 'Marking pending database message as captured.'); $dbMessage['captured'] = true; $db->storeMessage($dbMessage); } else { // Some kind of error in the request. We should keep the pending // db entry, complain loudly, and move this capture job to the // damaged queue. $this->logger->error("Failed to capture payment on account '{$this->account}' with reference " . "'{$this->pspReference}' and order id '{$this->merchantReference}'.", $dbMessage); $success = false; } break; case self::ACTION_REJECT: $this->cancelAuthorization(); // Delete the fraudy donor details $db->deleteMessage($dbMessage); break; case self::ACTION_DUPLICATE: // We have already captured one payment for this donation attempt, so // cancel the duplicate authorization. If there is a pending db entry, // leave it intact for the legitimate RecordCaptureJob. $this->cancelAuthorization(); break; case self::ACTION_REVIEW: // Don't capture the payment right now, but leave the donor details in // the pending database in case the authorization is captured via the console. break; case self::ACTION_MISSING: // Missing donor details - retry later throw new RetryableException('Missing donor details'); } return $success; }
public function execute() { $logger = Logger::getTaggedLogger("corr_id-adyen-{$this->merchantReference}"); $logger->info("Recording successful capture on account '{$this->account}' with authorization reference " . "'{$this->originalReference}' and order ID '{$this->merchantReference}'."); $config = Configuration::getDefaultConfig(); // Find the details from the payment site in the pending database. $logger->debug('Attempting to locate associated message in pending database'); $db = PendingDatabase::get(); $dbMessage = $db->fetchMessageByGatewayOrderId('adyen', $this->merchantReference); if ($dbMessage && isset($dbMessage['order_id'])) { $logger->debug('A valid message was obtained from the pending queue'); // Add the gateway transaction ID and send it to the completed queue $dbMessage['gateway_txn_id'] = $this->originalReference; $queueMessage = DonationInterfaceMessage::fromValues($dbMessage); SourceFields::addToMessage($queueMessage); $config->object('data-store/verified')->push($queueMessage); // Remove it from the pending database $logger->debug('Removing donor details message from pending database'); $db->deleteMessage($dbMessage); } else { // Sometimes we don't have a pending db row because the donor made // multiple attempts with the same order ID. It would be nice if // Adyen could prevent that, but let's not send a failmail since // we'll eventually get the donor details from the payments log // when we parse the audit. $logger->warning("Could not find donor details for authorization Reference '{$this->originalReference}' " . "and order ID '{$this->merchantReference}'.", $dbMessage); } return true; }
public static function instance($overrides = array()) { $config = self::createForViewWithOverrideFile('adyen', __DIR__ . '/config_test.yaml'); $config->override($overrides); // FIXME: What is this doing here? PendingDatabase::get()->createTable(); return $config; }
public function execute() { $logger = Logger::getTaggedLogger("corr_id-{$this->gateway}-{$this->order_id}"); $logger->info("Deleting message from pending db where gateway = '{$this->gateway}' " . "and order ID='{$this->order_id}'"); $deleteParams = array('gateway' => $this->gateway, 'order_id' => $this->order_id); PendingDatabase::get()->deleteMessage($deleteParams); return true; }
public function setUp() { parent::setUp(); $this->setMwGlobals(array('wgDonationInterfaceOrphanCron' => array('enable' => true, 'target_execute_time' => self::TARGET_EXECUTE_TIME, 'time_buffer' => self::TIME_BUFFER), 'wgGlobalCollectGatewayEnabled' => true, 'wgDonationInterfaceGatewayAdapters' => array('globalcollect' => 'TestingGlobalCollectOrphanAdapter', 'globalcollect_orphan' => 'TestingGlobalCollectOrphanAdapter'))); $config = SmashPigDatabaseTestConfiguration::instance(); Context::init($config); $this->pendingDb = PendingDatabase::get(); // Create the schema. $this->pendingDb->createTable(); }
public function testRecordCapture() { $verifiedQueue = BaseQueueConsumer::getQueue('verified'); $verifiedQueue->createTable('verified'); $capture = KeyedOpaqueStorableObject::fromJsonProxy('SmashPig\\PaymentProviders\\Adyen\\ExpatriatedMessages\\Capture', file_get_contents(__DIR__ . '/../Data/capture.json')); $job = RecordCaptureJob::factory($capture); $this->assertTrue($job->execute()); $donorData = $this->pendingDatabase->fetchMessageByGatewayOrderId('adyen', $capture->merchantReference); $this->assertNull($donorData, 'RecordCaptureJob left donor data on pending queue'); $verifiedMessage = $verifiedQueue->pop(); $this->assertNotNull($verifiedMessage, 'RecordCaptureJob did not send verified message'); // can we use arraySubset yet? $sameKeys = array_intersect(array_keys($verifiedMessage), array_keys($this->pendingMessage)); foreach ($sameKeys as $key) { if ($key === 'gateway_txn_id') { $this->assertEquals($capture->originalReference, $verifiedMessage[$key], 'RecordCaptureJob should have set gateway_txn_id'); } else { $this->assertEquals($this->pendingMessage[$key], $verifiedMessage[$key], "Value of key {$key} mutated"); } } }
/** * When two authorizations come in with the same merchant reference, we * should cancel the second one and leave the donor details in pending. */ public function testDuplicateAuthorisation() { $api = $this->config->object('payment-provider/adyen/api', true); $auth1 = KeyedOpaqueStorableObject::fromJsonProxy('SmashPig\\PaymentProviders\\Adyen\\ExpatriatedMessages\\Authorisation', file_get_contents(__DIR__ . '/../Data/auth.json')); $job1 = ProcessCaptureRequestJob::factory($auth1); $job1->execute(); $this->assertEquals(1, count($api->captured), 'Set up failed'); $auth2 = KeyedOpaqueStorableObject::fromJsonProxy('SmashPig\\PaymentProviders\\Adyen\\ExpatriatedMessages\\Authorisation', file_get_contents(__DIR__ . '/../Data/auth.json')); $auth2->pspReference = mt_rand(1000000000, 10000000000); $job2 = ProcessCaptureRequestJob::factory($auth2); $this->assertTrue($job2->execute(), 'Duplicate auths should not clutter damage queue'); $this->assertEquals(1, count($api->captured), 'Captured a duplicate!'); $this->assertEquals($auth2->pspReference, $api->cancelled[0], 'Did not cancel the right authorization'); $this->assertNotNull($this->pendingDatabase->fetchMessageByGatewayOrderId('adyen', $auth1->merchantReference), 'Capture job should leave donor details in database'); }
/** * Do the actual work of the script. */ public function execute() { $pendingDatabase = PendingDatabase::get(); $numCreated = 0; $limit = $this->getOptionOrConfig('max-messages', 'maintenance/create-ipns/message-limit'); $output = $this->getOption('output-dir'); $gateway = $this->getArgument('gateway'); $pendingMessages = $pendingDatabase->fetchMessagesByGatewayNewest($gateway, $limit); if (!$pendingMessages) { Logger::info("No pending database entries found for {$gateway}"); return; } $this->templateDir = __DIR__ . '/../Tests/IPNTemplates/' . $gateway . '/'; $templates = scandir($this->templateDir); foreach ($pendingMessages as $pendingMessage) { $this->createIpnMessages($pendingMessage, $templates, $output); $numCreated++; } Logger::info("Created {$numCreated} (sets of) IPN messages."); }
public function testDeleteMessage() { $uniq = mt_rand(); $message1 = $this->getTestMessage($uniq); // Store a second message for a good time, and make sure we delete the // right one. $message2 = $this->getTestMessage($uniq); $this->db->storeMessage($message1); $this->db->storeMessage($message2); // Confirm work without using the API. $pdo = $this->db->getDatabase(); $result = $pdo->query("\n\t\t\tselect * from pending\n\t\t\twhere gateway='test'\n\t\t\t\tand order_id = '{$message1['order_id']}'"); $rows = $result->fetchAll(PDO::FETCH_ASSOC); $this->assertEquals(2, count($rows), 'Both records were stored.'); $this->assertNotNull($rows[0]['id'], 'Record includes a primary row id'); $this->assertNotEquals($rows[0]['id'], $rows[1]['id'], 'Records have unique primary ids'); $this->db->deleteMessage($message1); // Confirm work without using the API. $pdo = $this->db->getDatabase(); $result = $pdo->query("\n\t\t\tselect * from pending\n\t\t\twhere gateway = 'test'\n\t\t\t\tand order_id = '{$message1['order_id']}'"); $rows = $result->fetchAll(PDO::FETCH_ASSOC); $this->assertEquals(0, count($rows), 'All rows deleted.'); }
public function testDifferentDatabases() { $pendingPdo = $this->pendingDb->getDatabase(); $initPdo = $this->paymentsInitialDb->getDatabase(); $this->assertNotEquals(spl_object_hash($pendingPdo), spl_object_hash($initPdo), 'Pending and paymentsInit databases share the same PDO'); }
/** * Remove a message from the pending database. * * @param array Normalized message */ protected function deleteMessage($message) { PendingDatabase::get()->deleteMessage($message); }