Skip to content

Commit 85a4373

Browse files
OctopusDeploy release: 14.0.2
1 parent 331bd38 commit 85a4373

File tree

4 files changed

+212
-11
lines changed

4 files changed

+212
-11
lines changed

CHANGELOG.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,11 @@
33
</a>
44

55
# Changelog
6-
## Latest Version v14.0.1 (11/13/25)
6+
## Latest Version v14.0.2 (11/20/25)
7+
### Enhancements:
8+
- [GPAPI] Enable GP-API access using Portico credentials
9+
10+
## v14.0.1 (11/13/25)
711
### Enhancements:
812
- [GPAPI] Added ContractReference field in Stored Credential
913

metadata.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
<xml>
2-
<releaseNumber>14.0.1</releaseNumber>
2+
<releaseNumber>14.0.2</releaseNumber>
33
</xml>

src/ServiceConfigs/Gateways/PorticoConfig.php

Lines changed: 51 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -59,18 +59,60 @@ public function __construct()
5959
$this->gatewayProvider = GatewayProvider::PORTICO;
6060
}
6161

62-
public function configureContainer(ConfiguredServices $services)
62+
/**
63+
* Validates service URL configuration and warns about potential mismatches
64+
*/
65+
private function validateServiceUrlConfiguration(): void
6366
{
64-
if (!empty($this->secretApiKey)) {
65-
if (strpos($this->secretApiKey, '_prod_') !== false) {
66-
$this->serviceUrl = ServiceEndpoints::PORTICO_PRODUCTION;
67-
} else {
68-
$this->serviceUrl = ServiceEndpoints::PORTICO_TEST;
69-
}
67+
if (empty($this->secretApiKey) || empty($this->serviceUrl)) {
68+
return;
7069
}
7170

71+
$hasProductionKey = str_contains($this->secretApiKey, '_prod_');
72+
$isTestEndpoint = str_contains($this->serviceUrl, ServiceEndpoints::PORTICO_TEST);
73+
$isProductionEndpoint = str_contains($this->serviceUrl, ServiceEndpoints::PORTICO_PRODUCTION);
74+
75+
if ($hasProductionKey && $isTestEndpoint) {
76+
$this->logConfigurationWarning(
77+
"Production API credentials detected with test/certification endpoint. " .
78+
"This configuration will route transactions to the test environment. " .
79+
"Verify this is intentional for testing purposes."
80+
);
81+
}
82+
83+
if (!$hasProductionKey && $isProductionEndpoint) {
84+
$this->logConfigurationWarning(
85+
"Test API credentials detected with production endpoint. " .
86+
"This may result in authentication failures."
87+
);
88+
}
89+
}
90+
91+
/**
92+
* Logs configuration warnings
93+
*/
94+
private function logConfigurationWarning(string $message): void
95+
{
96+
error_log("GlobalPayments PHP SDK - CONFIGURATION WARNING: $message");
97+
}
98+
99+
public function configureContainer(ConfiguredServices $services):void
100+
{
101+
// Validate for potential configuration mismatches and warn developer
102+
$this->validateServiceUrlConfiguration();
103+
104+
// Only auto-set serviceUrl if not explicitly configured
72105
if (empty($this->serviceUrl)) {
73-
$this->serviceUrl = $this->environment == Environment::TEST ? ServiceEndpoints::PORTICO_TEST : ServiceEndpoints::PORTICO_PRODUCTION; // check this
106+
if (!empty($this->secretApiKey)) {
107+
if (strpos($this->secretApiKey, '_prod_') !== false) {
108+
$this->serviceUrl = ServiceEndpoints::PORTICO_PRODUCTION;
109+
} else {
110+
$this->serviceUrl = ServiceEndpoints::PORTICO_TEST;
111+
}
112+
} else {
113+
$this->serviceUrl = $this->environment == Environment::TEST ?
114+
ServiceEndpoints::PORTICO_TEST : ServiceEndpoints::PORTICO_PRODUCTION;
115+
}
74116
}
75117

76118
$gateway = new PorticoConnector();
@@ -162,4 +204,4 @@ public function validate()
162204
);
163205
}
164206
}
165-
}
207+
}
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace GlobalPayments\Api\Tests\Integration\Gateways\PorticoConnector;
6+
7+
use GlobalPayments\Api\ServiceConfigs\Gateways\PorticoConfig;
8+
use GlobalPayments\Api\Entities\Enums\ServiceEndpoints;
9+
use GlobalPayments\Api\ServicesContainer;
10+
use GlobalPayments\Api\PaymentMethods\CreditCardData;
11+
use GlobalPayments\Api\Entities\Address;
12+
use PHPUnit\Framework\TestCase;
13+
14+
class PorticoEndpointConfigTest extends TestCase
15+
{
16+
private object $requestLogger;
17+
18+
protected function setUp(): void
19+
{
20+
parent::setUp();
21+
22+
// Create a mock request logger that captures URLs
23+
$this->requestLogger = new class implements \GlobalPayments\Api\Entities\IRequestLogger {
24+
public static array $urls = [];
25+
26+
public function requestSent($verb, $endpoint, $headers, $queryStringParams, $data): void {
27+
self::$urls[] = $endpoint;
28+
error_log("REQUEST CAPTURED: $verb $endpoint");
29+
}
30+
31+
public function responseReceived(\GlobalPayments\Api\Gateways\GatewayResponse $response): void {}
32+
33+
public function responseError(\Exception $e, $headers = ''): void {}
34+
};
35+
36+
// Clear previous URLs for this test
37+
$this->requestLogger::$urls = [];
38+
}
39+
40+
/**
41+
* CRITICAL: Verify production credentials with explicit CERT endpoint route to CERT
42+
*/
43+
public function testProdKeyWithCertEndpoint(): void
44+
{
45+
$config = new PorticoConfig();
46+
$config->secretApiKey = 'skapi_prod_9UnjAQz7gI5Bnl6MQq'; #gitleaks:allow
47+
$config->serviceUrl = ServiceEndpoints::PORTICO_TEST;
48+
$config->requestLogger = $this->requestLogger;
49+
50+
ServicesContainer::configureService($config, 'cert_test');
51+
52+
try {
53+
$this->createTestCard()->charge(10.00)
54+
->withCurrency('USD')
55+
->withAddress($this->createTestAddress())
56+
->execute('cert_test');
57+
} catch (\Exception) {
58+
// Expected to fail - we're testing URL routing
59+
}
60+
61+
$capturedUrl = $this->requestLogger::$urls[0] ?? '';
62+
$this->assertStringContainsString('cert.api2.heartlandportico.com', $capturedUrl);
63+
$this->assertStringNotContainsString('https://api2.heartlandportico.com/', $capturedUrl);
64+
}
65+
66+
/**
67+
* Verify that production credentials without explicit URL go to production
68+
*/
69+
public function testProdKeyAutoConfig(): void
70+
{
71+
$config = new PorticoConfig();
72+
$config->secretApiKey = 'skapi_prod_9UnjAQz7gI5Bnl6MQq'; #gitleaks:allow
73+
$config->requestLogger = $this->requestLogger;
74+
75+
ServicesContainer::configureService($config, 'prod_auto');
76+
77+
try {
78+
$this->createTestCard()
79+
->charge(10.00)
80+
->withCurrency('USD')
81+
->execute('prod_auto');
82+
} catch (\Exception) {
83+
// Expected to fail - we're testing URL routing
84+
}
85+
86+
$capturedUrl = $this->requestLogger::$urls[0] ?? '';
87+
$this->assertStringNotContainsString('cert.', $capturedUrl);
88+
$this->assertStringContainsString('api2.heartlandportico.com', $capturedUrl);
89+
}
90+
91+
/**
92+
* Critical bug scenario: Production credentials + explicit CERT must NOT hit production
93+
*/
94+
public function testBugFix(): void
95+
{
96+
$config = new PorticoConfig();
97+
$config->secretApiKey = 'skapi_prod_9UnjAQz7gI5Bnl6MQq'; #gitleaks:allow
98+
$config->serviceUrl = ServiceEndpoints::PORTICO_TEST;
99+
$config->requestLogger = $this->requestLogger;
100+
101+
ServicesContainer::configureService($config, 'bug_scenario');
102+
103+
try {
104+
$this->createTestCard()->charge(1.00)
105+
->withCurrency('USD')
106+
->withAddress($this->createTestAddress())
107+
->execute('bug_scenario');
108+
} catch (\Exception) {
109+
// Expected to fail - we're testing routing, not success
110+
}
111+
112+
$actualUrl = $this->requestLogger::$urls[0] ?? '';
113+
114+
$this->assertStringContainsString('cert.api2.heartlandportico.com', $actualUrl);
115+
$this->assertStringNotContainsString('https://api2.heartlandportico.com/', $actualUrl);
116+
$this->assertNotEquals(
117+
ServiceEndpoints::PORTICO_PRODUCTION . '/Hps.Exchange.PosGateway/PosGatewayService.asmx',
118+
$actualUrl
119+
);
120+
}
121+
122+
private function createTestCard(): CreditCardData
123+
{
124+
$card = new CreditCardData();
125+
$card->number = '4111111111111111';
126+
$card->expMonth = 12;
127+
$card->expYear = 2025;
128+
$card->cvn = '123';
129+
130+
return $card;
131+
}
132+
133+
private function createTestAddress(): Address
134+
{
135+
$address = new Address();
136+
$address->streetAddress1 = '123 Main St';
137+
$address->city = 'Downtown';
138+
$address->province = 'NJ';
139+
$address->postalCode = '12345';
140+
$address->country = 'USA';
141+
142+
return $address;
143+
}
144+
145+
protected function tearDown(): void
146+
{
147+
$configurations = ['cert_test', 'prod_auto', 'bug_scenario'];
148+
149+
foreach ($configurations as $config) {
150+
ServicesContainer::removeConfiguration($config);
151+
}
152+
153+
parent::tearDown();
154+
}
155+
}

0 commit comments

Comments
 (0)