Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/further payment validation #3

Merged
merged 6 commits into from
Oct 13, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ Versioning](https://semver.org/spec/v2.0.0.html).
### Added

- Added OS2Forms payment module
- Added further payment validation via NETS API

### Fixed

Expand Down
1 change: 1 addition & 0 deletions os2forms_payment.services.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ services:
arguments:
- '@request_stack'
- '@http_client'
- '@tempstore.private'

Drupal\os2forms_payment\Controller\NetsEasyController:
arguments:
Expand Down
169 changes: 141 additions & 28 deletions src/Helper/PaymentHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,13 @@

namespace Drupal\os2forms_payment\Helper;

use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Http\RequestStack;
use Drupal\Core\Site\Settings;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\TempStore\PrivateTempStore;
use Drupal\Core\TempStore\PrivateTempStoreFactory;
use Drupal\os2forms_payment\Plugin\WebformElement\NetsEasyPaymentElement;
use Drupal\webform\WebformSubmissionInterface;
use GuzzleHttp\ClientInterface;
use Psr\Http\Message\ResponseInterface;
Expand All @@ -13,13 +18,25 @@
*/
class PaymentHelper {

use StringTranslationTrait;
const AMOUNT_TO_PAY = 'AMOUNT_TO_PAY';

/**
* Private temp store.
*
* @var \Drupal\Core\TempStore\PrivateTempStore
*/
private readonly PrivateTempStore $privateTempStore;

/**
* {@inheritDoc}
*/
public function __construct(
private readonly RequestStack $requestStack,
private readonly ClientInterface $httpClient,
PrivateTempStoreFactory $tempStore,
) {
$this->privateTempStore = $tempStore->get('os2forms_payment');
}

/**
Expand All @@ -44,16 +61,20 @@ public function webformSubmissionPresave(WebFormSubmissionInterface $submission)
return;
}

$submissionData = $submission->getData();
$amountToPay = $this->getAmountToPay($submissionData, $paymentElement['#amount_to_pay']);
/*
* The paymentReferenceField is not a part of the form submission,
* so we get it from the POST payload.
* The goal here is to store the payment_id and amount_to_pay
* as a JSON object in the os2forms_payment submission value.
*/
$request = $this->requestStack->getCurrentRequest();
$paymentReferenceField = $request->request->get('os2forms_payment_reference_field');

/*
* The goal here is to store the payment_id, amount_to_pay and posting
* as a JSON object in the os2forms_payment submission value.
*/
$submissionData = $submission->getData();
$amountToPay = $this->getAmountToPay($submissionData, $paymentElement['#amount_to_pay']);

$paymentPosting = $paymentElement['#payment_posting'] ?? 'undefined';

if ($request && $amountToPay) {
Expand Down Expand Up @@ -94,16 +115,28 @@ public function getAmountToPay(array $values, string $key): float {
/**
* Validates a given payment via the Nets Payment API.
*
* @param string $endpoint
* Nets Payment API endpoint.
*
* @return bool
* Returns validation results.
* @param array<mixed> $element
* Element array.
* @param \Drupal\Core\Form\FormStateInterface $formState
* Form state object.
*/
public function validatePayment($endpoint): bool {
public function validatePayment(array &$element, FormStateInterface $formState): void {
// @FIXME: make error messages translateable.
jeppekroghitk marked this conversation as resolved.
Show resolved Hide resolved
$paymentId = $formState->getValue(NetsEasyPaymentElement::PAYMENT_REFERENCE_NAME);

if (!$paymentId) {
$formState->setError(
$element,
$this->t('No payment found.')
);
return;
}

$paymentEndpoint = $this->getPaymentEndpoint() . $paymentId;

$response = $this->httpClient->request(
'GET',
$endpoint,
$paymentEndpoint,
[
'headers' => [
'Content-Type' => 'application/json',
Expand All @@ -114,33 +147,78 @@ public function validatePayment($endpoint): bool {
);
$result = $this->responseToObject($response);

$reservedAmount = $result->payment->summary->reservedAmount ?? NULL;
$chargedAmount = $result->payment->summary->chargedAmount ?? NULL;
$amountToPay = floatval($this->getAmountToPayTemp() * 100);
$reservedAmount = floatval($result->payment->summary->reservedAmount ?? 0);
$chargedAmount = floatval($result->payment->summary->chargedAmount ?? 0);

if ($reservedAmount && !$chargedAmount) {
// Payment is reserved, but not yet charged.
$paymentCharged = $this->chargePayment($endpoint, $reservedAmount);
return $paymentCharged;
if ($amountToPay !== $reservedAmount) {
// Reserved amount mismatch.
$formState->setError(
$element,
$this->t('Reserved amount mismatch')
);
return;
}

if (!$reservedAmount && !$chargedAmount) {
// Reservation was not made.
return FALSE;
}
if ($reservedAmount > 0 && $chargedAmount == 0) {
// Payment is reserved, but not yet charged.
$paymentChargeId = $this->chargePayment($paymentEndpoint, $reservedAmount);
if (!$paymentChargeId) {
$formState->setError(
$element,
$this->t('Payment was not charged')
);
return;
}

return $reservedAmount === $chargedAmount;
/*
Right after charging the amount, the charge is validated via another
endpoint. Even though the charge is confirmed previously, due to
timing, the charge confirmation endpoint may return a 404 error.

Therefore, it is wrapped in a try/catch to allow it to pass,
even when it fails, essentially letting this serve as an optional check.
*/
jeppekroghitk marked this conversation as resolved.
Show resolved Hide resolved
try {
$chargeEndpoint = $this->getChargeEndpoint() . $paymentChargeId;
$response = $this->httpClient->request(
'GET',
$chargeEndpoint,
[
'headers' => [
'Authorization' => $this->getSecretKey(),
],
]
);

$result = $this->responseToObject($response);

$chargedAmountValidated = floatval($result->amount);

if ($reservedAmount !== $chargedAmountValidated) {
// Payment amount mismatch.
$formState->setError(
$element,
$this->t('Payment amount mismatch')
);
return;
}
}
catch (\Exception $e) {
}
}
}

/**
* Charges a given payment via the Nets Payment API.
*
* @param string $endpoint
* Nets Payment API endpoint.
* @param string $reservedAmount
* @param float $reservedAmount
* The reserved amount to be charged.
*
* @return bool
* Returns whether the payment was charged.
* @return string
* Returns charge id.
*/
private function chargePayment($endpoint, $reservedAmount) {
$endpoint = $endpoint . '/charges';
Expand All @@ -162,7 +240,7 @@ private function chargePayment($endpoint, $reservedAmount) {
);

$result = $this->responseToObject($response);
return (bool) $result->chargeId;
return $result->chargeId;
}

/**
Expand Down Expand Up @@ -223,8 +301,20 @@ public function getTestMode(): bool {
*/
public function getPaymentEndpoint(): string {
return $this->getTestMode()
? 'https://test.api.dibspayment.eu/v1/payments/'
: 'https://api.dibspayment.eu/v1/payments/';
? 'https://test.api.dibspayment.eu/v1/payments/'
: 'https://api.dibspayment.eu/v1/payments/';
}

/**
* Returns the Nets API charge endpoint.
*
* @return string
* The endpoint URL.
*/
public function getChargeEndpoint(): string {
return $this->getTestMode()
? 'https://test.api.dibspayment.eu/v1/charges/'
: 'https://api.dibspayment.eu/v1/charges/';
}

/**
Expand All @@ -237,4 +327,27 @@ public function responseToObject(ResponseInterface $response): object {
return json_decode($response->getBody()->getContents());
}

/**
* Sets the amount to pay in private tempoary storage.
*
* @param float $amountToPay
* The amount to pay.
*
* @return void
* Sets the tempoary storage.
*/
public function setAmountToPayTemp(float $amountToPay): void {
$this->privateTempStore->set(self::AMOUNT_TO_PAY, $amountToPay);
}

/**
* Gets the amount to pay in private tempoary storage.
*
* @return float
* The amount to pay.
*/
public function getAmountToPayTemp(): float {
return $this->privateTempStore->get(self::AMOUNT_TO_PAY);
}

}
29 changes: 5 additions & 24 deletions src/Plugin/WebformElement/NetsEasyPaymentElement.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ class NetsEasyPaymentElement extends WebformElementBase {
*/
private PaymentHelper $paymentHelper;

public const PAYMENT_REFERENCE_NAME = 'os2forms_payment_reference_field';
const PAYMENT_REFERENCE_NAME = 'os2forms_payment_reference_field';

/**
* {@inheritdoc}
Expand Down Expand Up @@ -126,8 +126,8 @@ public function alterForm(array &$element, array &$form, FormStateInterface $for
$webformCurrentPage = $form['progress']['#current_page'];
// Check if we are on the preview page.
if ($webformCurrentPage === "webform_preview") {

$amountToPay = $this->paymentHelper->getAmountToPay($formState->getUserInput(), $this->getElementProperty($element, 'amount_to_pay'));
$this->paymentHelper->setAmountToPayTemp($amountToPay);
/*
* If amount to pay is present,
* inject placeholder for nets gateway containing amount to pay.
Expand All @@ -137,7 +137,6 @@ public function alterForm(array &$element, array &$form, FormStateInterface $for
}

$paymentMethods = array_values(array_filter($element['#payment_methods'] ?? []));

$paymentPosting = $element['#payment_posting'] ?? 'undefined';

$form['os2forms_payment_checkout_container'] = [
Expand All @@ -155,7 +154,7 @@ public function alterForm(array &$element, array &$form, FormStateInterface $for
])->toString(TRUE)->getGeneratedUrl(),
],
'#limit_validation_errors' => [],
'#element_validate' => [[get_class($this), 'validatePayment']],
'#element_validate' => [[$this::class, 'validatePayment']],
];
$form['os2forms_payment_reference_field'] = [
'#type' => 'hidden',
Expand All @@ -176,27 +175,9 @@ public function alterForm(array &$element, array &$form, FormStateInterface $for
* Returns validation results.
*/
public static function validatePayment(array &$element, FormStateInterface $formState): mixed {
$paymentHelper = \Drupal::service('Drupal\os2forms_payment\Helper\PaymentHelper');
$paymentElementClass = get_called_class();

$paymentId = $formState->getValue($paymentElementClass::PAYMENT_REFERENCE_NAME);
if (!$paymentId) {
return $formState->setError(
$element,
t('The form could not be submitted. Please try again.')
);
}
$paymentHelper = \Drupal::service(PaymentHelper::class);

$endpoint = $paymentHelper->getPaymentEndpoint() . $paymentId;

$paymentValidated = $paymentHelper->validatePayment($endpoint);

if (!$paymentValidated) {
return $formState->setError(
$element,
t('The payment could not be validated. Please try again.')
);
}
$paymentHelper->validatePayment($element, $formState);

return TRUE;
}
Expand Down
Loading