From 3a283fa67c31cf2d3031d54fc2b51773d99ba1b4 Mon Sep 17 00:00:00 2001 From: Dennis Eichhorn Date: Sat, 8 Apr 2023 04:36:26 +0200 Subject: [PATCH 1/4] fix billing process --- .github/user_bug_report.md | 14 +- .../Install/Application/Shop/Application.php | 12 +- Controller/ApiController.php | 120 +++++++++++------- Theme/Backend/Lang/de.lang.php | 23 ++++ Theme/Backend/Lang/en.lang.php | 2 + 5 files changed, 110 insertions(+), 61 deletions(-) create mode 100644 Theme/Backend/Lang/de.lang.php diff --git a/.github/user_bug_report.md b/.github/user_bug_report.md index 9e5f2a5..4b92a8e 100755 --- a/.github/user_bug_report.md +++ b/.github/user_bug_report.md @@ -8,9 +8,11 @@ assignees: '' --- # Bug Description + A clear and concise description of what the bug is. # How to Reproduce + Steps to reproduce the behavior: 1. Go to '...' @@ -19,16 +21,20 @@ Steps to reproduce the behavior: 4. See error # Expected Behavior + A clear and concise description of what you expected to happen. # Screenshots + If applicable, add screenshots to help explain your problem. # System Information - - System: [e.g. PC or iPhone11, ...] - - OS: [e.g. iOS] - - Browser [e.g. chrome, safari] - - KarakaVersion [e.g. 22] + +- System: [e.g. PC or iPhone11, ...] +- OS: [e.g. iOS] +- Browser [e.g. chrome, safari] +- KarakaVersion [e.g. 22] # Additional Information + Add any other context about the problem here. diff --git a/Admin/Install/Application/Shop/Application.php b/Admin/Install/Application/Shop/Application.php index 896cf49..7051a18 100755 --- a/Admin/Install/Application/Shop/Application.php +++ b/Admin/Install/Application/Shop/Application.php @@ -153,18 +153,14 @@ final class Application if (!($account instanceof NullAccount)) { $response->header->l11n = $account->l11n; - } elseif ($this->app->sessionManager->get('language') !== null) { + } elseif ($this->app->sessionManager->get('language') !== null + && $response->header->l11n->getLanguage() !== $this->app->sessionManager->get('language') + ) { $response->header->l11n ->loadFromLanguage( $this->app->sessionManager->get('language'), $this->app->sessionManager->get('country') ?? '*' ); - } elseif ($this->app->cookieJar->get('language') !== null) { - $response->header->l11n - ->loadFromLanguage( - $this->app->cookieJar->get('language'), - $this->app->cookieJar->get('country') ?? '*' - ); } if (!\in_array($response->getLanguage(), $this->config['language'])) { @@ -212,7 +208,7 @@ final class Application $request->uri->getRoute(), $request->getDataString('CSRF'), $request->getRouteVerb(), - $this->app->appName, + $this->app->appId, $this->app->unitId, $account, $request->getData() diff --git a/Controller/ApiController.php b/Controller/ApiController.php index dc0a998..30b55c6 100755 --- a/Controller/ApiController.php +++ b/Controller/ApiController.php @@ -16,6 +16,7 @@ namespace Modules\Shop\Controller; use Modules\Billing\Models\Attribute\BillAttributeTypeMapper; use Modules\Billing\Models\Bill; +use Modules\Billing\Models\BillMapper; use Modules\ClientManagement\Models\ClientMapper; use Modules\ClientManagement\Models\NullClient; use Modules\ItemManagement\Models\Item; @@ -25,9 +26,9 @@ use Modules\Payment\Models\PaymentMapper; use Modules\Payment\Models\PaymentStatus; use phpOMS\Autoloader; use phpOMS\Localization\ISO4217CharEnum; -use phpOMS\Localization\ISO4217SymbolEnum; use phpOMS\Message\Http\HttpRequest; use phpOMS\Message\Http\HttpResponse; +use phpOMS\Message\Http\RequestStatusCode; use phpOMS\Message\RequestAbstract; use phpOMS\Message\ResponseAbstract; use phpOMS\System\MimeType; @@ -58,13 +59,13 @@ final class ApiController extends Controller */ public function apiSchemaCreate(RequestAbstract $request, ResponseAbstract $response, mixed $data = null) : void { - $schema = $this->buildSchema(new NullItem()); + $schema = $this->buildSchema(new NullItem(), $request); $response->header->set('Content-Type', MimeType::M_JSON . '; charset=utf-8', true); $response->set($request->uri->__toString(), $schema); } - public function buildSchema(Item $item) : array + public function buildSchema(Item $item, RequestAbstract $request) : array { $images = $item->getFilesByTypeName('shop_primary_image'); @@ -86,7 +87,7 @@ final class ApiController extends Controller ]; foreach ($images as $image) { - $schema['image'][] = $image->getPath(); + $schema['image'][] = $request->uri->getBase() . '/' . $image->getPath(); } return $schema; @@ -107,18 +108,12 @@ final class ApiController extends Controller */ public function apiOneClickBuy(RequestAbstract $request, ResponseAbstract $response, mixed $data = null) : void { - $item = $request->hasData('item') - ? ItemMapper::get()->where('id', (int) $request->getData('item'))->execute() - : ItemMapper::get()->where('number', (string) $request->getData('number'))->execute(); - - // get item - // get client - // get client data - // get payment data - // create one-click-shoping-cart = invoice - // create payment based on client data - $client = ClientMapper::get() + ->with('mainAddress') + ->with('attributes') + ->with('attributes/type') + ->with('attributes/value') + ->with('account') ->where('account', $request->header->account) ->execute(); @@ -134,15 +129,34 @@ final class ApiController extends Controller $paymentInfo = $paymentInfoMapper->execute(); } - $bill = new Bill(); + $request->setData('client', $client->getId(), true); + $bill = $this->app->moduleManager->get('Billing', 'ApiBill')->createBaseBill($client, $request); - // add item to bill - // set quantity - // set price - // attach payment to bill - // set external payment id to bill - // execute bill payment + $itemMapper = $request->hasData('item') + ? ItemMapper::get() + ->where('id', (int) $request->getData('item')) + : ItemMapper::get() + ->where('number', (string) $request->getData('number')); + $itemMapper->with('l11n') + ->with('attributes') + ->with('attributes/type') + ->with('attributes/value') + ->with('l11n/type') + ->where('l11n/type/title', ['name1', 'name2', 'name3'], 'IN') + ->where('l11n/language', $bill->getLanguage()) + ->execute(); + + $item = $itemMapper->execute(); + + // @todo: consider to first create an offer = cart and only when paid turn it into an invoice. This way it's also easy to analyse the conversion rate. + + $billElement = $this->app->moduleManager->get('Billing', 'ApiBill')->createBaseBillElement($client, $item, $bill, $request); + $bill->addElement($billElement); + + $this->app->moduleManager->get('Billing', 'ApiBill')->createBillDatabaseEntry($bill, $request); + + // @tood: make this configurable (either from the customer payment info or some item default setting)!!! $this->setupStripe($request, $response, $bill, $data); } @@ -168,8 +182,8 @@ final class ApiController extends Controller $this->app->moduleManager->get('Billing', 'ApiAttribute')->apiBillAttributeCreate($internalRequest, $internalResponse, $data); // Redirect to stripe checkout page + $response->header->status = RequestStatusCode::R_303; $response->header->set('Content-Type', MimeType::M_JSON, true); - $response->header->set('', 'HTTP/1.1 303 See Other', true); $response->header->set('Location', $session->url, true); } @@ -188,7 +202,7 @@ final class ApiController extends Controller $api_key = $_SERVER['OMS_STRIPE_SECRET'] ?? ''; $endpoint_secret = $_SERVER['OMS_STRIPE_PUBLIC'] ?? ''; - $include = \realpath(__DIR__ . '/../../../Resources/'); + $include = \realpath(__DIR__ . '/../../../Resources/Stripe'); if (empty($api_key) || empty($endpoint_secret) || $include === false) { return null; @@ -196,34 +210,42 @@ final class ApiController extends Controller Autoloader::addPath($include); + $stripeData = [ + 'line_items' => [], + 'mode' => 'payment', + 'currency' => $bill->getCurrency(), + 'success_url' => $success, + 'cancel_url' => $cancel, + 'client_reference_id' => $bill->number, + // 'customer' => 'stripe_customer_id...', + 'customer_email' => $bill->client->account->getEmail(), + ]; + + $elements = $bill->getElements(); + foreach ($elements as $element) { + $stripeData['line_items'][] = [ + 'quantity' => 1, + 'price_data' => [ + 'tax_behavior' => 'inclusive', + 'currency' => $bill->getCurrency(), + 'unit_amount' => (int) ($element->totalSalesPriceGross->getInt() / 100), + //'amount_subtotal' => (int) ($bill->netSales->getInt() / 100), + //'amount_total' => (int) ($bill->grossSales->getInt() / 100), + 'product_data' => [ + 'name' => $element->itemName, + 'metadata' => [ + 'pro_id' => $element->itemNumber, + ], + ], + ] + ]; + } + //$stripe = new \Stripe\StripeClient($api_key); \Stripe\Stripe::setApiKey($api_key); - $session = \Stripe\Checkout\Session::create([ - 'line_items' => [[ - 'amount_subtotal' => '...', - 'amount_tax' => '...', - 'amount_total' => '...', - 'quantity' => 1, - 'price_data' => [ - 'name' => '', - 'metadata' => [ - 'pro_id' => 0, - ] - ], - 'unit_amount' => 0, - 'currency' => 0, - ]], - 'amount_subtotal' => '...', - 'amount_total' => '...', - 'mode' => 'payment', - 'currency' => '...', - 'success_url' => $success, - 'cancel_url' => $cancel, - 'client_reference_id' => '...', - 'customer' => 'stripe_customer_id...', - 'customer_email' => 'customer_email...', - ]); + // @todo: instead of using account email, use client billing email if defined and only use account email as fallback + $session = \Stripe\Checkout\Session::create($stripeData); return $session; } diff --git a/Theme/Backend/Lang/de.lang.php b/Theme/Backend/Lang/de.lang.php new file mode 100644 index 0000000..104524e --- /dev/null +++ b/Theme/Backend/Lang/de.lang.php @@ -0,0 +1,23 @@ + [ + 'Price' => 'Preis', + 'Buy' => 'Kaufen', + 'Website' => 'Webseite', + 'Demo' => 'Demo', + 'Download' => 'Download', + 'Net' => 'Netto', + 'Gross' => 'Brutto', +]]; diff --git a/Theme/Backend/Lang/en.lang.php b/Theme/Backend/Lang/en.lang.php index cbae332..dffd60c 100755 --- a/Theme/Backend/Lang/en.lang.php +++ b/Theme/Backend/Lang/en.lang.php @@ -18,4 +18,6 @@ return ['Shop' => [ 'Website' => 'Website', 'Demo' => 'Demo', 'Download' => 'Download', + 'Net' => 'Net', + 'Gross' => 'Gross', ]]; From 5f3717bc27a523203632daaeb8663e00a8dae61f Mon Sep 17 00:00:00 2001 From: Dennis Eichhorn Date: Sun, 9 Apr 2023 06:03:39 +0200 Subject: [PATCH 2/4] fix static analysis --- Controller/ApiController.php | 51 +++++++++++++++++++++++++++++++----- 1 file changed, 44 insertions(+), 7 deletions(-) diff --git a/Controller/ApiController.php b/Controller/ApiController.php index 30b55c6..dca099f 100755 --- a/Controller/ApiController.php +++ b/Controller/ApiController.php @@ -16,7 +16,6 @@ namespace Modules\Shop\Controller; use Modules\Billing\Models\Attribute\BillAttributeTypeMapper; use Modules\Billing\Models\Bill; -use Modules\Billing\Models\BillMapper; use Modules\ClientManagement\Models\ClientMapper; use Modules\ClientManagement\Models\NullClient; use Modules\ItemManagement\Models\Item; @@ -65,6 +64,16 @@ final class ApiController extends Controller $response->set($request->uri->__toString(), $schema); } + /** + * Method to create a schema from an item + * + * @param Item $item Item to create the schema from + * @param RequestAbstract $request Request + * + * @return array + * + * @since 1.0.0 + */ public function buildSchema(Item $item, RequestAbstract $request) : array { $images = $item->getFilesByTypeName('shop_primary_image'); @@ -108,6 +117,7 @@ final class ApiController extends Controller */ public function apiOneClickBuy(RequestAbstract $request, ResponseAbstract $response, mixed $data = null) : void { + /** @var \Modules\ClientManagement\Models\Client $client */ $client = ClientMapper::get() ->with('mainAddress') ->with('attributes') @@ -144,9 +154,9 @@ final class ApiController extends Controller ->with('attributes/value') ->with('l11n/type') ->where('l11n/type/title', ['name1', 'name2', 'name3'], 'IN') - ->where('l11n/language', $bill->getLanguage()) - ->execute(); + ->where('l11n/language', $bill->getLanguage()); + /** @var \Modules\ItemManagement\Models\Item $item */ $item = $itemMapper->execute(); // @todo: consider to first create an offer = cart and only when paid turn it into an invoice. This way it's also easy to analyse the conversion rate. @@ -160,19 +170,34 @@ final class ApiController extends Controller $this->setupStripe($request, $response, $bill, $data); } + /** + * Create stripe checkout response + * + * @param RequestAbstract $request Request + * @param ResponseAbstract $response Response + * @param Bill $bill Bill + * @param mixed $data Generic data + * + * @return void + * + * @since 1.0.0 + */ private function setupStripe( RequestAbstract $request, ResponseAbstract $response, Bill $bill, mixed $data = null - ) : void { + ) : void + { $session = $this->createStripeSession($bill, $data['success'], $data['cancel']); // Assign payment id to bill /** \Modules\Billing\Models\Attribute\BillAttributeType $type */ - $type = BillAttributeTypeMapper::get()->where('name', 'external_payment_id')->execute(); + $type = BillAttributeTypeMapper::get() + ->where('name', 'external_payment_id') + ->execute(); - $internalRequest = new HttpRequest(new HttpUri('')); + $internalRequest = new HttpRequest(new HttpUri('')); $internalResponse = new HttpResponse(); $internalRequest->header->account = $request->header->account; @@ -187,11 +212,23 @@ final class ApiController extends Controller $response->header->set('Location', $session->url, true); } + /** + * Create stripe session + * + * @param Bill $bill Bill + * @param string $success Success url + * @param string $cancel Cancel url + * + * @return \Stripe\Checkout\Session|null + * + * @since 1.0.0 + */ private function createStripeSession( Bill $bill, string $success, string $cancel - ) { + ) : ?\Stripe\Checkout\Session + { // $this->app->appSettings->getEncrypted() // $stripeSecretKeyTemp = $this->app->appSettings->get(); From b79b52b1f12c154ef7626c522cb2874d6697aacf Mon Sep 17 00:00:00 2001 From: Dennis Eichhorn Date: Tue, 11 Apr 2023 00:20:13 +0200 Subject: [PATCH 3/4] fix static analysis --- .github/user_feature_request.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/user_feature_request.md b/.github/user_feature_request.md index 6eb8ddc..c9595e8 100755 --- a/.github/user_feature_request.md +++ b/.github/user_feature_request.md @@ -8,11 +8,14 @@ assignees: '' --- # What is the feature you request + * A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] * A clear and concise description of what you want to happen. # Alternatives + A clear and concise description of any alternative solutions or features you've considered. # Additional Information + Add any other context or screenshots about the feature request here. From 3ab7b2b87cbe0da475371f98bbecdff556316353 Mon Sep 17 00:00:00 2001 From: Dennis Eichhorn Date: Sun, 16 Apr 2023 01:55:35 +0200 Subject: [PATCH 4/4] fix demoSetup --- Controller/ApiController.php | 126 ++--------------------------------- info.json | 3 +- 2 files changed, 7 insertions(+), 122 deletions(-) diff --git a/Controller/ApiController.php b/Controller/ApiController.php index dca099f..fbdc79e 100755 --- a/Controller/ApiController.php +++ b/Controller/ApiController.php @@ -14,8 +14,6 @@ declare(strict_types=1); namespace Modules\Shop\Controller; -use Modules\Billing\Models\Attribute\BillAttributeTypeMapper; -use Modules\Billing\Models\Bill; use Modules\ClientManagement\Models\ClientMapper; use Modules\ClientManagement\Models\NullClient; use Modules\ItemManagement\Models\Item; @@ -141,6 +139,9 @@ final class ApiController extends Controller $request->setData('client', $client->getId(), true); $bill = $this->app->moduleManager->get('Billing', 'ApiBill')->createBaseBill($client, $request); + $this->app->moduleManager->get('Billing', 'ApiBill')->createBillDatabaseEntry($bill, $request); + + $old = clone $bill; $itemMapper = $request->hasData('item') ? ItemMapper::get() @@ -164,126 +165,9 @@ final class ApiController extends Controller $billElement = $this->app->moduleManager->get('Billing', 'ApiBill')->createBaseBillElement($client, $item, $bill, $request); $bill->addElement($billElement); - $this->app->moduleManager->get('Billing', 'ApiBill')->createBillDatabaseEntry($bill, $request); + $this->updateModel($request->header->account, $old, $bill, BillMapper::class, 'bill_element', $request->getOrigin()); // @tood: make this configurable (either from the customer payment info or some item default setting)!!! - $this->setupStripe($request, $response, $bill, $data); - } - - /** - * Create stripe checkout response - * - * @param RequestAbstract $request Request - * @param ResponseAbstract $response Response - * @param Bill $bill Bill - * @param mixed $data Generic data - * - * @return void - * - * @since 1.0.0 - */ - private function setupStripe( - RequestAbstract $request, - ResponseAbstract $response, - Bill $bill, - mixed $data = null - ) : void - { - $session = $this->createStripeSession($bill, $data['success'], $data['cancel']); - - // Assign payment id to bill - /** \Modules\Billing\Models\Attribute\BillAttributeType $type */ - $type = BillAttributeTypeMapper::get() - ->where('name', 'external_payment_id') - ->execute(); - - $internalRequest = new HttpRequest(new HttpUri('')); - $internalResponse = new HttpResponse(); - - $internalRequest->header->account = $request->header->account; - $internalRequest->setData('type', $type->getId()); - $internalRequest->setData('custom', (string) $session->id); - $internalRequest->setData('bill', $bill->getId()); - $this->app->moduleManager->get('Billing', 'ApiAttribute')->apiBillAttributeCreate($internalRequest, $internalResponse, $data); - - // Redirect to stripe checkout page - $response->header->status = RequestStatusCode::R_303; - $response->header->set('Content-Type', MimeType::M_JSON, true); - $response->header->set('Location', $session->url, true); - } - - /** - * Create stripe session - * - * @param Bill $bill Bill - * @param string $success Success url - * @param string $cancel Cancel url - * - * @return \Stripe\Checkout\Session|null - * - * @since 1.0.0 - */ - private function createStripeSession( - Bill $bill, - string $success, - string $cancel - ) : ?\Stripe\Checkout\Session - { - // $this->app->appSettings->getEncrypted() - - // $stripeSecretKeyTemp = $this->app->appSettings->get(); - // $stripeSecretKey = $this->app->appSettings->decrypt($stripeSecretKeyTemp); - - // \Stripe\Stripe::setApiKey($stripeSecretKey); - - $api_key = $_SERVER['OMS_STRIPE_SECRET'] ?? ''; - $endpoint_secret = $_SERVER['OMS_STRIPE_PUBLIC'] ?? ''; - - $include = \realpath(__DIR__ . '/../../../Resources/Stripe'); - - if (empty($api_key) || empty($endpoint_secret) || $include === false) { - return null; - } - - Autoloader::addPath($include); - - $stripeData = [ - 'line_items' => [], - 'mode' => 'payment', - 'currency' => $bill->getCurrency(), - 'success_url' => $success, - 'cancel_url' => $cancel, - 'client_reference_id' => $bill->number, - // 'customer' => 'stripe_customer_id...', - 'customer_email' => $bill->client->account->getEmail(), - ]; - - $elements = $bill->getElements(); - foreach ($elements as $element) { - $stripeData['line_items'][] = [ - 'quantity' => 1, - 'price_data' => [ - 'tax_behavior' => 'inclusive', - 'currency' => $bill->getCurrency(), - 'unit_amount' => (int) ($element->totalSalesPriceGross->getInt() / 100), - //'amount_subtotal' => (int) ($bill->netSales->getInt() / 100), - //'amount_total' => (int) ($bill->grossSales->getInt() / 100), - 'product_data' => [ - 'name' => $element->itemName, - 'metadata' => [ - 'pro_id' => $element->itemNumber, - ], - ], - ] - ]; - } - - //$stripe = new \Stripe\StripeClient($api_key); - \Stripe\Stripe::setApiKey($api_key); - - // @todo: instead of using account email, use client billing email if defined and only use account email as fallback - $session = \Stripe\Checkout\Session::create($stripeData); - - return $session; + $this->app->moduleManager->get('Payment', 'Api')->setupStripe($request, $response, $bill, $data); } } diff --git a/info.json b/info.json index 12f13e0..ccee466 100755 --- a/info.json +++ b/info.json @@ -22,7 +22,8 @@ }, "providing": { "CMS": "*", - "Media": "*" + "Media": "*", + "Payment": "*" }, "load": [ {