From d598a9214f19ec679913c4ec6e8fb38fd3c568bd Mon Sep 17 00:00:00 2001 From: Thomas Peterson Date: Tue, 13 Jan 2026 11:04:34 +0100 Subject: [PATCH] Fixes+ --- src/new/fixtures/product.yml | 12 +- .../create.html.twig | 0 .../delete.html.twig | 0 .../edit.html.twig | 0 .../step1.html.twig | 0 .../step2.html.twig | 0 .../step3.html.twig | 0 .../Repository/VoucherItemRepository.php | 35 +++++ .../src/PSC/Shop/OrderBundle/Service/Calc.php | 5 +- .../PSC/Shop/OrderBundle/Service/VatCalc.php | 38 +++-- .../PSC/Shop/Order/Api/CreateVatTest.php | 139 ++++++++++++++++++ .../PSC/Shop/Order/Service/ImportCalcTest.php | 14 +- .../CreateOrderFromExistingProductTest.php | 60 ++++---- .../Gutschein/Form/Field/ProductSettings.php | 113 ++++++++++++++ .../Gutschein/Model/ProductSpecialObject.php | 14 ++ .../PSC/Gutschein/Producer/Producer.php | 50 ++++++- .../Gutschein/Resources/config/services.yml | 4 + .../Resources/views/form/field/calc.html.twig | 16 +- .../form/field/product_settings.html.twig | 29 ++++ .../Service/VoucherCodeDistributor.php | 103 +++++++++++++ .../PSC/Gutschein/Transformer/Product.php | 8 + .../System/PSC/XmlCalc/Producer/Producer.php | 11 +- 22 files changed, 573 insertions(+), 78 deletions(-) rename src/new/src/PSC/Shop/ContactBundle/Resources/views/backend/{address_detail => addressdetail}/create.html.twig (100%) mode change 100755 => 100644 rename src/new/src/PSC/Shop/ContactBundle/Resources/views/backend/{address_detail => addressdetail}/delete.html.twig (100%) mode change 100755 => 100644 rename src/new/src/PSC/Shop/ContactBundle/Resources/views/backend/{address_detail => addressdetail}/edit.html.twig (100%) mode change 100755 => 100644 rename src/new/src/PSC/Shop/ContactBundle/Resources/views/backend/{address_import => addressimport}/step1.html.twig (100%) mode change 100755 => 100644 rename src/new/src/PSC/Shop/ContactBundle/Resources/views/backend/{address_import => addressimport}/step2.html.twig (100%) mode change 100755 => 100644 rename src/new/src/PSC/Shop/ContactBundle/Resources/views/backend/{address_import => addressimport}/step3.html.twig (100%) mode change 100755 => 100644 create mode 100644 src/new/tests/PSC/Shop/Order/Api/CreateVatTest.php create mode 100644 src/new/var/plugins/Custom/PSC/Gutschein/Form/Field/ProductSettings.php create mode 100644 src/new/var/plugins/Custom/PSC/Gutschein/Resources/views/form/field/product_settings.html.twig create mode 100644 src/new/var/plugins/Custom/PSC/Gutschein/Service/VoucherCodeDistributor.php diff --git a/src/new/fixtures/product.yml b/src/new/fixtures/product.yml index d6cd8e4a0..b3cbe4b84 100755 --- a/src/new/fixtures/product.yml +++ b/src/new/fixtures/product.yml @@ -9,7 +9,7 @@ PSC\Shop\EntityBundle\Entity\Product: pos: notEdit: false private: false - taxClass: 19 + mwert: 19 price: 0 set_config: '{}' shop: '@shop_1' @@ -23,7 +23,7 @@ PSC\Shop\EntityBundle\Entity\Product: notEdit: false pos: private: false - taxClass: 19 + mwert: 19 price: 0 set_config: '{}' shop: '@shop_1' @@ -37,7 +37,7 @@ PSC\Shop\EntityBundle\Entity\Product: notEdit: false pos: private: false - taxClass: 19 + mwert: 19 price: 0 set_config: '\[{"article_id": "7996f00d-a5e6-4915-970d-9f7c4287cd92"}, {"article_id": "7996f00d-a5e6-4915-970d-9f7c4287cd91"} ]' shop: '@shop_1' @@ -51,7 +51,7 @@ PSC\Shop\EntityBundle\Entity\Product: type: 2 pos: private: false - taxClass: 19 + mwert: 19 price: 20 set_config: '{}' shop: '@shop_1' @@ -65,7 +65,7 @@ PSC\Shop\EntityBundle\Entity\Product: pos: notEdit: false private: false - taxClass: 19 + mwert: 19 set_config: '{}' shop: '@shop_1' calcXml: > @@ -116,7 +116,7 @@ PSC\Shop\EntityBundle\Entity\Product: pos: notEdit: false private: false - taxClass: 19 + mwert: 19 set_config: '{}' shop: '@shop_1' calcXml: > diff --git a/src/new/src/PSC/Shop/ContactBundle/Resources/views/backend/address_detail/create.html.twig b/src/new/src/PSC/Shop/ContactBundle/Resources/views/backend/addressdetail/create.html.twig old mode 100755 new mode 100644 similarity index 100% rename from src/new/src/PSC/Shop/ContactBundle/Resources/views/backend/address_detail/create.html.twig rename to src/new/src/PSC/Shop/ContactBundle/Resources/views/backend/addressdetail/create.html.twig diff --git a/src/new/src/PSC/Shop/ContactBundle/Resources/views/backend/address_detail/delete.html.twig b/src/new/src/PSC/Shop/ContactBundle/Resources/views/backend/addressdetail/delete.html.twig old mode 100755 new mode 100644 similarity index 100% rename from src/new/src/PSC/Shop/ContactBundle/Resources/views/backend/address_detail/delete.html.twig rename to src/new/src/PSC/Shop/ContactBundle/Resources/views/backend/addressdetail/delete.html.twig diff --git a/src/new/src/PSC/Shop/ContactBundle/Resources/views/backend/address_detail/edit.html.twig b/src/new/src/PSC/Shop/ContactBundle/Resources/views/backend/addressdetail/edit.html.twig old mode 100755 new mode 100644 similarity index 100% rename from src/new/src/PSC/Shop/ContactBundle/Resources/views/backend/address_detail/edit.html.twig rename to src/new/src/PSC/Shop/ContactBundle/Resources/views/backend/addressdetail/edit.html.twig diff --git a/src/new/src/PSC/Shop/ContactBundle/Resources/views/backend/address_import/step1.html.twig b/src/new/src/PSC/Shop/ContactBundle/Resources/views/backend/addressimport/step1.html.twig old mode 100755 new mode 100644 similarity index 100% rename from src/new/src/PSC/Shop/ContactBundle/Resources/views/backend/address_import/step1.html.twig rename to src/new/src/PSC/Shop/ContactBundle/Resources/views/backend/addressimport/step1.html.twig diff --git a/src/new/src/PSC/Shop/ContactBundle/Resources/views/backend/address_import/step2.html.twig b/src/new/src/PSC/Shop/ContactBundle/Resources/views/backend/addressimport/step2.html.twig old mode 100755 new mode 100644 similarity index 100% rename from src/new/src/PSC/Shop/ContactBundle/Resources/views/backend/address_import/step2.html.twig rename to src/new/src/PSC/Shop/ContactBundle/Resources/views/backend/addressimport/step2.html.twig diff --git a/src/new/src/PSC/Shop/ContactBundle/Resources/views/backend/address_import/step3.html.twig b/src/new/src/PSC/Shop/ContactBundle/Resources/views/backend/addressimport/step3.html.twig old mode 100755 new mode 100644 similarity index 100% rename from src/new/src/PSC/Shop/ContactBundle/Resources/views/backend/address_import/step3.html.twig rename to src/new/src/PSC/Shop/ContactBundle/Resources/views/backend/addressimport/step3.html.twig diff --git a/src/new/src/PSC/Shop/EntityBundle/Repository/VoucherItemRepository.php b/src/new/src/PSC/Shop/EntityBundle/Repository/VoucherItemRepository.php index f891b88b0..9f244990f 100755 --- a/src/new/src/PSC/Shop/EntityBundle/Repository/VoucherItemRepository.php +++ b/src/new/src/PSC/Shop/EntityBundle/Repository/VoucherItemRepository.php @@ -34,4 +34,39 @@ class VoucherItemRepository extends EntityRepository ->getQuery()->execute(); return $isDeleted; } + + /** + * Count unused codes for a voucher + * + * @param int $voucherUid + * @return int + */ + public function countUnusedCodes(int $voucherUid): int + { + return (int) $this->createQueryBuilder('vi') + ->select('COUNT(vi.uid)') + ->where('vi.voucher = :voucherId') + ->andWhere('vi.used = false') + ->setParameter('voucherId', $voucherUid) + ->getQuery() + ->getSingleScalarResult(); + } + + /** + * Find unused VoucherItems (for display only, not for locking) + * + * @param int $voucherUid + * @param int $limit + * @return array + */ + public function findUnusedCodes(int $voucherUid, int $limit = 100): array + { + return $this->createQueryBuilder('vi') + ->where('vi.voucher = :voucherId') + ->andWhere('vi.used = false') + ->setParameter('voucherId', $voucherUid) + ->setMaxResults($limit) + ->getQuery() + ->getResult(); + } } diff --git a/src/new/src/PSC/Shop/OrderBundle/Service/Calc.php b/src/new/src/PSC/Shop/OrderBundle/Service/Calc.php index c715605ee..e91ad2153 100755 --- a/src/new/src/PSC/Shop/OrderBundle/Service/Calc.php +++ b/src/new/src/PSC/Shop/OrderBundle/Service/Calc.php @@ -63,9 +63,7 @@ readonly class Calc $order->addTax($order->getShipping()->getCalcPrice()->tax); foreach ($order->getPositions() as $position) { $position->getProduct()->setShopUuid($order->getShop()->getUuid()); - if ( - $this->productTypeRegistry->getProductType($position->getProduct()->getSpecialProductTypeObject()->getTyp()) - ) { + if ($this->productTypeRegistry->getProductType($position->getProduct()->getSpecialProductTypeObject()->getTyp())) { $specialProductTransformer = $this->productTypeRegistry ->getProductType($position->getProduct()->getSpecialProductTypeObject()->getTyp()) ->getProducer(); @@ -77,7 +75,6 @@ readonly class Calc $specialProductTransformer->calcPriceForOrderPosition($position); } } - $priceNet = $priceNet->plus(Money::ofMinor($position->getPrice()->getAllNet(), 'EUR')); $priceVat = $priceVat->plus(Money::ofMinor($position->getPrice()->getAllVat(), 'EUR')); $priceGross = $priceGross->plus(Money::ofMinor($position->getPrice()->getAllGross(), 'EUR')); diff --git a/src/new/src/PSC/Shop/OrderBundle/Service/VatCalc.php b/src/new/src/PSC/Shop/OrderBundle/Service/VatCalc.php index 4ced238d4..c2ccfe5dc 100755 --- a/src/new/src/PSC/Shop/OrderBundle/Service/VatCalc.php +++ b/src/new/src/PSC/Shop/OrderBundle/Service/VatCalc.php @@ -5,37 +5,44 @@ namespace PSC\Shop\OrderBundle\Service; use PSC\Shop\EntityBundle\Document\Country; use PSC\Shop\EntityBundle\Repository\CountryRepository; use PSC\Shop\OrderBundle\Model\Order\Tax; +use PSC\System\SettingsBundle\Service\Shop; class VatCalc { - - public function __construct(private readonly CountryRepository $countryRepository) - { - } + public function __construct( + private readonly CountryRepository $countryRepository, + private readonly Shop $shopService, + ) {} public function calcVat(\PSC\Shop\OrderBundle\Model\Base $order): void { - $country = strtoupper($order->getDeliveryAddress()->getCountry()); $ustid = $order->getDeliveryAddress()->getUstid(); - if($country == '') { + if ($country == '') { $country = 'DE'; } - - /** - * @var Country $countryDoc -*/ - $countryDoc = $this->countryRepository->findOneBy(['code' => $country]); + if ($order->getShop()->getId() == 0) { + $shop = $this->shopService->getShopByUid($order->getShop()->getUuid()); + $countryDoc = $this->countryRepository->findOneBy([ + 'code' => $country, + 'shop' => $shop->getId(), + ]); + } else { + $countryDoc = $this->countryRepository->findOneBy([ + 'code' => $country, + 'shop' => $order->getShop()->getId(), + ]); + } $useVat = true; - if($ustid != "") { + if ($ustid != '') { $useVat = $countryDoc->isWithTaxWithUstNr(); - }else{ + } else { $useVat = $countryDoc->isWithTaxWithoutUstNr(); } - if(!$useVat) { + if (!$useVat) { $order->getPayment()->getCalcPrice()->vat = 0; $order->getPayment()->getCalcPrice()->gross = $order->getPayment()->getCalcPrice()->net; $order->getPayment()->getCalcPrice()->tax = new Tax(); @@ -44,7 +51,7 @@ class VatCalc $order->getShipping()->getCalcPrice()->gross = $order->getPayment()->getCalcPrice()->net; $order->getShipping()->getCalcPrice()->tax = new Tax(); - foreach($order->getPositions() as $position) { + foreach ($order->getPositions() as $position) { $position->getPrice()->setVat(0); $position->getPrice()->setGross(0); $position->getPrice()->setAllVat($position->getPrice()->getNet()); @@ -57,5 +64,4 @@ class VatCalc $order->setTaxes([]); } } - } diff --git a/src/new/tests/PSC/Shop/Order/Api/CreateVatTest.php b/src/new/tests/PSC/Shop/Order/Api/CreateVatTest.php new file mode 100644 index 000000000..ac0613d73 --- /dev/null +++ b/src/new/tests/PSC/Shop/Order/Api/CreateVatTest.php @@ -0,0 +1,139 @@ +get(ShopRepository::class); + + /** + * @var Shop $shop + */ + $shop = $shopRepository->findOneBy(['title' => 'Printchampion']); + + $shippingRepository = static::getContainer()->get(ShippingRepository::class); + + $paymentRepository = static::getContainer()->get(PaymentRepository::class); + + $client->jsonRequest( + 'POST', + '/api/order/create', + [ + 'shop' => [ + 'uuid' => (string) $shop->getUuid(), + ], + 'type' => 2, + 'shipping' => [ + 'uid' => $shippingRepository->findOneBy(['title' => 'Abholung vor Ort'])->getUid(), + ], + 'payment' => [ + 'uid' => $paymentRepository->findOneBy(['title' => 'Bar'])->getUid(), + ], + 'draft' => false, + 'deliveryAddress' => [ + 'firstname' => 'Thomas', + 'lastname' => 'Peterson', + 'street' => 'Chausseestr.', + 'houseNumber' => '24', + 'zip' => '17506', + 'city' => 'Gribow', + ], + 'invoiceAddress' => [ + 'firstname' => 'Thomas', + 'lastname' => 'Peterson', + 'street' => 'Chausseestr.', + 'houseNumber' => '24', + 'zip' => '17400', + 'city' => 'Berlin', + ], + 'positions' => [ + [ + 'count' => 1, + 'product' => [ + 'title' => 'test XML', + 'specialProductTypeObject' => [ + 'typ' => 6, + 'taxClass' => 700, + 'xml' => ' + + + Blocks A5 25blatt geleimt + kein + + + + + ', + ], + ], + ], + [ + 'count' => 1, + 'product' => [ + 'title' => 'test Manual Position', + 'specialProductTypeObject' => [ + 'typ' => 1, + 'cent' => true, + 'net' => 14500, + 'taxClass' => 700, + ], + ], + ], + ], + ], + ['HTTP_apiKey' => $shop->getApiKey()], + ); + + self::assertSame(200, $client->getResponse()->getStatusCode()); + + $data = json_decode($client->getResponse()->getContent(), true); + + $client->jsonRequest( + 'POST', + '/api/order/getonebyuuid', + [ + 'uuid' => $data['uuid'], + ], + ['HTTP_apiKey' => $shop->getApiKey()], + ); + + $data = json_decode($client->getResponse()->getContent(), true); + self::assertSame(20865, $data['gross']); + self::assertSame('SAN-' . date('Ym') . '-1', $data['alias']); + + self::assertSame('Berlin', $data['invoiceAddress']['city']); + + self::assertSame('Gribow', $data['deliveryAddress']['city']); + + self::assertSame('ShopMusterOrt', $data['senderAddress']['city']); + self::assertSame('ShopMusterIban', $data['senderAddress']['iban']); + + self::assertSame(200, $client->getResponse()->getStatusCode()); + + /** + * @var JobRepository $jobs + */ + $jobs = static::getContainer()->get(JobRepository::class); + + self::assertCount(0, $jobs->findBy(['data.order' => $data['uuid']])); + } +} diff --git a/src/new/tests/PSC/Shop/Order/Service/ImportCalcTest.php b/src/new/tests/PSC/Shop/Order/Service/ImportCalcTest.php index 89719858e..cdd7872bf 100644 --- a/src/new/tests/PSC/Shop/Order/Service/ImportCalcTest.php +++ b/src/new/tests/PSC/Shop/Order/Service/ImportCalcTest.php @@ -2,17 +2,17 @@ namespace App\Tests\PSC\Shop\Order\Service; -use PSC\Shop\OrderBundle\Model\Order\Position\Price; -use Tests\RefreshDatabaseTrait; use PSC\Component\ApiBundle\Model\Shop; use PSC\Shop\ContactBundle\Model\Address; use PSC\Shop\OrderBundle\Model\Order\Position; +use PSC\Shop\OrderBundle\Model\Order\Position\Price; use PSC\Shop\OrderBundle\Service\Calc; use PSC\Shop\PaymentBundle\Model\Payment; use PSC\Shop\ProductBundle\Model\Product; use PSC\Shop\ProductBundle\Model\ProductSpecialObject; use PSC\Shop\ShippingBundle\Model\Shipping; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; +use Tests\RefreshDatabaseTrait; class ImportCalcTest extends KernelTestCase { @@ -25,12 +25,12 @@ class ImportCalcTest extends KernelTestCase $container = static::getContainer(); /** - * @var Calc $calcService -*/ + * @var Calc $calcService + */ $calcService = $container->get(Calc::class); $shop = new Shop(); - $shop->setUuid('shop1'); + $shop->setUuid('771a1176-d531-48ed-93b8-eec1fd4b917f'); $order = new \PSC\Shop\OrderBundle\Model\Order(); $order->setShop($shop); @@ -57,7 +57,7 @@ class ImportCalcTest extends KernelTestCase $specialProductSettingsW1 = new ProductSpecialObject(); $specialProductSettingsW1->setCount(1); - $specialProductSettingsW1->setNet(25.5*100); + $specialProductSettingsW1->setNet(25.5 * 100); $specialProductSettingsW1->setCent(true); $productW1->setSpecialProductTypeObject($specialProductSettingsW1); $price1 = new Price(); @@ -67,7 +67,7 @@ class ImportCalcTest extends KernelTestCase $positionW1 = new Position(); $positionW1->setProduct($productW1); - $positionW1->setPrice($price1); + $positionW1->setPrice($price1); $order->addPosition($positionW1); diff --git a/src/new/tests/Plugins/System/PSC/XmlCalc/Api/CreateOrderFromExistingProductTest.php b/src/new/tests/Plugins/System/PSC/XmlCalc/Api/CreateOrderFromExistingProductTest.php index 559accf02..716133630 100644 --- a/src/new/tests/Plugins/System/PSC/XmlCalc/Api/CreateOrderFromExistingProductTest.php +++ b/src/new/tests/Plugins/System/PSC/XmlCalc/Api/CreateOrderFromExistingProductTest.php @@ -3,14 +3,14 @@ namespace Plugins\System\PSC\XmlCalc\Api; use PSC\Shop\ContactBundle\Model\AccountType; -use PSC\Shop\EntityBundle\Repository\JobRepository; -use Tests\RefreshDatabaseTrait; use PSC\Shop\ContactBundle\Repository\ContactRepository; use PSC\Shop\EntityBundle\Entity\Shop; +use PSC\Shop\EntityBundle\Repository\JobRepository; use PSC\Shop\EntityBundle\Repository\ShopRepository; use PSC\Shop\PaymentBundle\Repository\PaymentRepository; use PSC\Shop\ShippingBundle\Repository\ShippingRepository; use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; +use Tests\RefreshDatabaseTrait; class CreateOrderFromExistingProductTest extends WebTestCase { @@ -33,14 +33,14 @@ class CreateOrderFromExistingProductTest extends WebTestCase '/api/order/create', [ 'shop' => [ - 'uuid' => (string)$shop->getUuid() + 'uuid' => (string) $shop->getUuid(), ], 'type' => 2, 'shipping' => [ - 'uid' => $shippingRepository->findOneBy(['title' => 'Abholung vor Ort'])->getUid() + 'uid' => $shippingRepository->findOneBy(['title' => 'Abholung vor Ort'])->getUid(), ], 'payment' => [ - 'uid' => $paymentRepository->findOneBy(['title' => 'Bar bei Abholung'])->getUid() + 'uid' => $paymentRepository->findOneBy(['title' => 'Bar bei Abholung'])->getUid(), ], 'draft' => false, 'deliveryAddress' => [ @@ -49,7 +49,7 @@ class CreateOrderFromExistingProductTest extends WebTestCase 'street' => 'Chausseestr.', 'houseNumber' => '24', 'zip' => '17506', - 'city' => 'Gribow' + 'city' => 'Gribow', ], 'invoiceAddress' => [ 'firstname' => 'Thomas', @@ -57,7 +57,7 @@ class CreateOrderFromExistingProductTest extends WebTestCase 'street' => 'Chausseestr.', 'houseNumber' => '24', 'zip' => '17400', - 'city' => 'Berlin' + 'city' => 'Berlin', ], 'positions' => [ [ @@ -68,14 +68,14 @@ class CreateOrderFromExistingProductTest extends WebTestCase 'specialProductTypeObject' => [ 'typ' => 6, 'params' => [ - 'auflage' => 100 + 'auflage' => 100, ], - ] - ] - ] - ] + ], + ], + ], + ], ], - ['HTTP_apiKey' => $shop->getApiKey()] + ['HTTP_apiKey' => $shop->getApiKey()], ); self::assertSame(200, $client->getResponse()->getStatusCode()); @@ -86,9 +86,9 @@ class CreateOrderFromExistingProductTest extends WebTestCase 'POST', '/api/order/getonebyuuid', [ - 'uuid' => $data['uuid'], + 'uuid' => $data['uuid'], ], - ['HTTP_apiKey' => $shop->getApiKey()] + ['HTTP_apiKey' => $shop->getApiKey()], ); self::assertSame(200, $client->getResponse()->getStatusCode()); @@ -117,17 +117,17 @@ class CreateOrderFromExistingProductTest extends WebTestCase '/api/order/create', [ 'shop' => [ - 'uuid' => (string)$shop->getUuid() + 'uuid' => (string) $shop->getUuid(), ], 'type' => 2, 'shipping' => [ - 'uid' => $shippingRepository->findOneBy(['title' => 'Abholung vor Ort'])->getUid() + 'uid' => $shippingRepository->findOneBy(['title' => 'Abholung vor Ort'])->getUid(), ], 'payment' => [ - 'uid' => $paymentRepository->findOneBy(['title' => 'Bar bei Abholung'])->getUid() + 'uid' => $paymentRepository->findOneBy(['title' => 'Bar bei Abholung'])->getUid(), ], 'contact' => [ - 'accountType' => AccountType::COMPANY + 'accountType' => AccountType::COMPANY, ], 'draft' => false, 'deliveryAddress' => [ @@ -136,7 +136,7 @@ class CreateOrderFromExistingProductTest extends WebTestCase 'street' => 'Chausseestr.', 'houseNumber' => '24', 'zip' => '17506', - 'city' => 'Gribow' + 'city' => 'Gribow', ], 'invoiceAddress' => [ 'firstname' => 'Thomas', @@ -144,7 +144,7 @@ class CreateOrderFromExistingProductTest extends WebTestCase 'street' => 'Chausseestr.', 'houseNumber' => '24', 'zip' => '17400', - 'city' => 'Berlin' + 'city' => 'Berlin', ], 'positions' => [ [ @@ -155,14 +155,14 @@ class CreateOrderFromExistingProductTest extends WebTestCase 'specialProductTypeObject' => [ 'typ' => 6, 'params' => [ - 'auflage' => 100 + 'auflage' => 100, ], - ] - ] - ] - ] + ], + ], + ], + ], ], - ['HTTP_apiKey' => $shop->getApiKey()] + ['HTTP_apiKey' => $shop->getApiKey()], ); self::assertSame(200, $client->getResponse()->getStatusCode()); @@ -174,9 +174,9 @@ class CreateOrderFromExistingProductTest extends WebTestCase 'POST', '/api/order/getonebyuuid', [ - 'uuid' => $data['uuid'], + 'uuid' => $data['uuid'], ], - ['HTTP_apiKey' => $shop->getApiKey()] + ['HTTP_apiKey' => $shop->getApiKey()], ); self::assertSame(200, $client->getResponse()->getStatusCode()); @@ -187,6 +187,4 @@ class CreateOrderFromExistingProductTest extends WebTestCase $jobs = static::getContainer()->get(JobRepository::class); self::assertCount(0, $jobs->findBy(['data.order' => $data['uuid']])); } - - } diff --git a/src/new/var/plugins/Custom/PSC/Gutschein/Form/Field/ProductSettings.php b/src/new/var/plugins/Custom/PSC/Gutschein/Form/Field/ProductSettings.php new file mode 100644 index 000000000..413192f47 --- /dev/null +++ b/src/new/var/plugins/Custom/PSC/Gutschein/Form/Field/ProductSettings.php @@ -0,0 +1,113 @@ +entityManager->getRepository(Voucher::class); + /** @var VoucherItemRepository $voucherItemRepo */ + $voucherItemRepo = $this->entityManager->getRepository(\PSC\Shop\EntityBundle\Entity\VoucherItem::class); + + $vouchers = $voucherRepo->findAll(); + $choices = ['-- Zufallscode generieren (Standard) --' => '']; + + foreach ($vouchers as $voucher) { + $unusedCount = $voucherItemRepo->countUnusedCodes($voucher->getUid()); + $label = sprintf( + '%s (%d verfügbare Codes)', + $voucher->getTitle(), + $unusedCount + ); + $choices[$label] = (string)$voucher->getUid(); + } + + $currentLinkedVoucher = null; + if ($product && isset($product->gutschein['linkedVoucherUid'])) { + $currentLinkedVoucher = (string)$product->gutschein['linkedVoucherUid']; + } + + $builder->add('linkedVoucherUid', ChoiceType::class, [ + 'required' => false, + 'label' => 'Verknüpfter Gutschein', + 'choices' => $choices, + 'data' => $currentLinkedVoucher, + 'help' => 'Wählen Sie einen Gutschein aus dem VoucherBundle, um vordefinierte Codes zu verwenden. Wenn kein Gutschein ausgewählt ist, wird ein Zufallscode generiert.', + ]); + + return $builder; + } + + public function getGroup() + { + return \Plugin\Custom\PSC\Gutschein\Form\Group\Calc::GROUP_ID; + } + + public function formPostSetData(FormEvent $event) + { + } + + public function formPostSubmit(FormEvent $event) + { + /** @var Product $product */ + $product = $event->getData(); + $form = $event->getForm(); + + if ($form->has('linkedVoucherUid')) { + $linkedVoucherUid = $form->get('linkedVoucherUid')->getData(); + + // Get existing gutschein data + $gutscheinData = $product->gutschein ?? []; + + // Update or set linkedVoucherUid + if (!empty($linkedVoucherUid)) { + $gutscheinData['linkedVoucherUid'] = (int)$linkedVoucherUid; + } else { + // Remove if empty (use random generation) + unset($gutscheinData['linkedVoucherUid']); + } + + // Save back to product + $product->gutschein = $gutscheinData; + } + } + + public function formPreSetData(FormEvent $event) + { + } + + public function formSubmit(FormEvent $event) + { + } +} diff --git a/src/new/var/plugins/Custom/PSC/Gutschein/Model/ProductSpecialObject.php b/src/new/var/plugins/Custom/PSC/Gutschein/Model/ProductSpecialObject.php index b6622c932..1893d32ae 100644 --- a/src/new/var/plugins/Custom/PSC/Gutschein/Model/ProductSpecialObject.php +++ b/src/new/var/plugins/Custom/PSC/Gutschein/Model/ProductSpecialObject.php @@ -25,6 +25,9 @@ class ProductSpecialObject implements IProductTypeObject private string $voucherCode = ''; private ?\DateTime $expirationDate = null; + // VoucherBundle integration + private ?int $linkedVoucherUid = null; + public function getName(): string { return 'Gutscheinprodukt'; @@ -46,6 +49,7 @@ class ProductSpecialObject implements IProductTypeObject 'voucherCode' => $this->voucherCode, 'expirationDate' => $this->expirationDate?->format('Y-m-d H:i:s'), 'validityMonths' => $this->validityMonths, + 'linkedVoucherUid' => $this->linkedVoucherUid, ]; } @@ -169,4 +173,14 @@ class ProductSpecialObject implements IProductTypeObject { $this->expirationDate = $expirationDate; } + + public function getLinkedVoucherUid(): ?int + { + return $this->linkedVoucherUid; + } + + public function setLinkedVoucherUid(?int $linkedVoucherUid): void + { + $this->linkedVoucherUid = $linkedVoucherUid; + } } diff --git a/src/new/var/plugins/Custom/PSC/Gutschein/Producer/Producer.php b/src/new/var/plugins/Custom/PSC/Gutschein/Producer/Producer.php index bdcf7249a..38d3b56ec 100644 --- a/src/new/var/plugins/Custom/PSC/Gutschein/Producer/Producer.php +++ b/src/new/var/plugins/Custom/PSC/Gutschein/Producer/Producer.php @@ -29,6 +29,7 @@ class Producer implements IUiProducer, IProducerHydrateModel public function __construct( private readonly EntityManagerInterface $entityManager, private readonly CodeGenerator $codeGenerator, + private readonly \Plugin\Custom\PSC\Gutschein\Service\VoucherCodeDistributor $voucherCodeDistributor, ) {} public function setProduct(Product $product): void @@ -136,11 +137,54 @@ class Producer implements IUiProducer, IProducerHydrateModel public function calcPriceForOrderPosition(Position $position): void { - // Generate unique voucher code for this order position - $voucherCode = $this->codeGenerator->generate(); - /** @var ProductSpecialObject $specProd */ $specProd = $position->getProduct()->getSpecialProductTypeObject(); + + $voucherCode = null; + + // Try to get code from VoucherBundle if product is linked + if ($specProd->getLinkedVoucherUid()) { + try { + $contact = $position->getOrder()?->getContact(); + $voucherItem = $this->voucherCodeDistributor->getNextAvailableCode( + $specProd->getLinkedVoucherUid(), + $contact + ); + + if ($voucherItem) { + // Successfully got code from VoucherBundle + $voucherCode = $voucherItem->getCode(); + + error_log(sprintf( + '[Gutschein] Distributed VoucherBundle code %s from Voucher %d for Product %s', + $voucherCode, + $specProd->getLinkedVoucherUid(), + $position->getProduct()->getUuid() + )); + } else { + // No codes available - log warning + error_log(sprintf( + '[Gutschein] WARNING: No available codes in VoucherBundle Voucher %d for Product %s, using random generation', + $specProd->getLinkedVoucherUid(), + $position->getProduct()->getUuid() + )); + } + } catch (\Exception $e) { + // Error accessing VoucherBundle - log and fall back + error_log(sprintf( + '[Gutschein] ERROR: Failed to access VoucherBundle (Voucher %d): %s, using random generation', + $specProd->getLinkedVoucherUid(), + $e->getMessage() + )); + } + } + + // Fall back to random generation if no VoucherBundle code obtained + if (!$voucherCode) { + $voucherCode = $this->codeGenerator->generate(); + } + + // Set voucher details $specProd->setVoucherCode($voucherCode); $specProd->setVoucherAmount($this->voucherAmount); $specProd->setRecipientName($this->recipientName); diff --git a/src/new/var/plugins/Custom/PSC/Gutschein/Resources/config/services.yml b/src/new/var/plugins/Custom/PSC/Gutschein/Resources/config/services.yml index 0ebbb98a6..bcbc22429 100644 --- a/src/new/var/plugins/Custom/PSC/Gutschein/Resources/config/services.yml +++ b/src/new/var/plugins/Custom/PSC/Gutschein/Resources/config/services.yml @@ -17,3 +17,7 @@ services: Plugin\Custom\PSC\Gutschein\Form\Field\Calc: tags: - { name: psc.backend.custom.fields, productType: 9 } + + Plugin\Custom\PSC\Gutschein\Form\Field\ProductSettings: + tags: + - { name: psc.backend.custom.fields, productType: 9 } diff --git a/src/new/var/plugins/Custom/PSC/Gutschein/Resources/views/form/field/calc.html.twig b/src/new/var/plugins/Custom/PSC/Gutschein/Resources/views/form/field/calc.html.twig index e972b5cf6..8d4fc98be 100644 --- a/src/new/var/plugins/Custom/PSC/Gutschein/Resources/views/form/field/calc.html.twig +++ b/src/new/var/plugins/Custom/PSC/Gutschein/Resources/views/form/field/calc.html.twig @@ -6,8 +6,8 @@
- {{ form_label(form.gutschein.validityMonths) }} - {{ form_widget(form.gutschein.validityMonths) }} + {{ form_label(form.validityMonths) }} + {{ form_widget(form.validityMonths) }} Gültigkeitsdauer ab Kaufdatum
@@ -16,22 +16,22 @@
- {{ form_label(form.gutschein.minAmount) }} - {{ form_widget(form.gutschein.minAmount) }} + {{ form_label(form.minAmount) }} + {{ form_widget(form.minAmount) }} Mindestbetrag
- {{ form_label(form.gutschein.defaultAmount) }} - {{ form_widget(form.gutschein.defaultAmount) }} + {{ form_label(form.defaultAmount) }} + {{ form_widget(form.defaultAmount) }} Vorauswahl
- {{ form_label(form.gutschein.maxAmount) }} - {{ form_widget(form.gutschein.maxAmount) }} + {{ form_label(form.maxAmount) }} + {{ form_widget(form.maxAmount) }} Maximalbetrag
diff --git a/src/new/var/plugins/Custom/PSC/Gutschein/Resources/views/form/field/product_settings.html.twig b/src/new/var/plugins/Custom/PSC/Gutschein/Resources/views/form/field/product_settings.html.twig new file mode 100644 index 000000000..5c9f3dc0e --- /dev/null +++ b/src/new/var/plugins/Custom/PSC/Gutschein/Resources/views/form/field/product_settings.html.twig @@ -0,0 +1,29 @@ +
+
+
VoucherBundle Integration
+
+
+
+ {{ form_label(form.linkedVoucherUid) }} + {{ form_widget(form.linkedVoucherUid, {'attr': {'class': 'form-control'}}) }} + {{ form_help(form.linkedVoucherUid) }} +
+ +
+ Wie funktioniert die Verknüpfung? +
    +
  • Wählen Sie einen Gutschein aus dem VoucherBundle aus, um vordefinierte Codes zu verwenden
  • +
  • Bei jedem Verkauf wird automatisch ein Code aus dem Voucher zugewiesen
  • +
  • Der Code wird als "verwendet" markiert und mit dem Kunden verknüpft
  • +
  • Wenn keine Codes mehr verfügbar sind, wird automatisch ein Zufallscode generiert
  • +
  • Ohne Verknüpfung werden weiterhin Zufallscodes generiert (Standard-Verhalten)
  • +
+
+ + {% if form.linkedVoucherUid.vars.value %} +
+ Aktuell verknüpft: Dieser Gutschein verwendet Codes aus dem ausgewählten VoucherBundle-Gutschein. +
+ {% endif %} +
+
diff --git a/src/new/var/plugins/Custom/PSC/Gutschein/Service/VoucherCodeDistributor.php b/src/new/var/plugins/Custom/PSC/Gutschein/Service/VoucherCodeDistributor.php new file mode 100644 index 000000000..4c8e2d418 --- /dev/null +++ b/src/new/var/plugins/Custom/PSC/Gutschein/Service/VoucherCodeDistributor.php @@ -0,0 +1,103 @@ + + * @copyright 2012-2013 PrintshopCreator GmbH + * @license Private + * @link http://www.printshopcreator.de + */ + +namespace Plugin\Custom\PSC\Gutschein\Service; + +use Doctrine\DBAL\LockMode; +use Doctrine\ORM\EntityManagerInterface; +use PSC\Shop\EntityBundle\Entity\VoucherItem; +use PSC\Shop\EntityBundle\Entity\Contact; + +/** + * VoucherCodeDistributor + * + * Service for atomic code distribution from VoucherBundle + * Uses pessimistic locking to prevent race conditions + */ +class VoucherCodeDistributor +{ + public function __construct( + private readonly EntityManagerInterface $entityManager + ) {} + + /** + * Get next available unused code from a Voucher + * Uses pessimistic locking to prevent race conditions + * + * @param int $voucherUid + * @param Contact|null $contact + * @return VoucherItem|null + * @throws \Exception + */ + public function getNextAvailableCode(int $voucherUid, ?Contact $contact = null): ?VoucherItem + { + $this->entityManager->beginTransaction(); + + try { + // Use pessimistic write lock for atomic assignment + $qb = $this->entityManager->createQueryBuilder(); + $voucherItem = $qb->select('vi') + ->from(VoucherItem::class, 'vi') + ->where('vi.voucher = :voucherId') + ->andWhere('vi.used = :used') + ->setParameter('voucherId', $voucherUid) + ->setParameter('used', false) + ->setMaxResults(1) + ->getQuery() + ->setLockMode(LockMode::PESSIMISTIC_WRITE) + ->getOneOrNullResult(); + + if (!$voucherItem) { + $this->entityManager->rollback(); + return null; + } + + // Mark as used and link to contact + $voucherItem->setUsed(true); + if ($contact) { + $voucherItem->setContact($contact); + } + $voucherItem->setUpdatedAt(new \DateTime()); + + $this->entityManager->flush(); + $this->entityManager->commit(); + + return $voucherItem; + + } catch (\Exception $e) { + $this->entityManager->rollback(); + throw $e; + } + } + + /** + * Check if a Voucher has available codes + * + * @param int $voucherUid + * @return bool + */ + public function hasAvailableCodes(int $voucherUid): bool + { + return $this->getRemainingCodeCount($voucherUid) > 0; + } + + /** + * Get count of remaining unused codes + * + * @param int $voucherUid + * @return int + */ + public function getRemainingCodeCount(int $voucherUid): int + { + $repo = $this->entityManager->getRepository(VoucherItem::class); + return $repo->countUnusedCodes($voucherUid); + } +} diff --git a/src/new/var/plugins/Custom/PSC/Gutschein/Transformer/Product.php b/src/new/var/plugins/Custom/PSC/Gutschein/Transformer/Product.php index bc5052b76..af7364271 100644 --- a/src/new/var/plugins/Custom/PSC/Gutschein/Transformer/Product.php +++ b/src/new/var/plugins/Custom/PSC/Gutschein/Transformer/Product.php @@ -35,6 +35,10 @@ class Product implements IProductTransformer if ($productDoc->getCustom4()) { $prodSpec->setValidityMonths((int)$productDoc->getCustom4()); } + // Load linked VoucherBundle Voucher UID from custom5 + if ($productDoc->getCustom5()) { + $prodSpec->setLinkedVoucherUid((int)$productDoc->getCustom5()); + } } public function toDb( @@ -50,5 +54,9 @@ class Product implements IProductTransformer $productDoc->setCustom2((string)$prodSpec->getMaxAmount()); $productDoc->setCustom3((string)$prodSpec->getDefaultAmount()); $productDoc->setCustom4((string)$prodSpec->getValidityMonths()); + // Save linked VoucherBundle Voucher UID to custom5 + $productDoc->setCustom5( + $prodSpec->getLinkedVoucherUid() ? (string)$prodSpec->getLinkedVoucherUid() : '' + ); } } diff --git a/src/new/var/plugins/System/PSC/XmlCalc/Producer/Producer.php b/src/new/var/plugins/System/PSC/XmlCalc/Producer/Producer.php index 7c80193dd..eede28656 100755 --- a/src/new/var/plugins/System/PSC/XmlCalc/Producer/Producer.php +++ b/src/new/var/plugins/System/PSC/XmlCalc/Producer/Producer.php @@ -32,7 +32,7 @@ class Producer implements IUiProducer, IProducerHydrateModel, ICalcNeedContact private Product $product; private Engine $engine; private EntityManagerInterface $entityManager; - private null|Contact $contact = null; + private ?Contact $contact = null; public function __construct( Shop $shopService, @@ -73,7 +73,6 @@ class Producer implements IUiProducer, IProducerHydrateModel, ICalcNeedContact * @var ProductSpecialObject $specProd */ $specProd = $this->product->getSpecialProductTypeObject(); - $priceObj = Money::ofMinor($this->engine->getPrice() * 100, 'EUR'); $price = new Price(); @@ -152,6 +151,9 @@ class Producer implements IUiProducer, IProducerHydrateModel, ICalcNeedContact $engine->loadString($product->getCalcXml()); $engine->setFormulas($product->getShop()->getFormel()); $engine->setParameters($product->getShop()->getParameter()); + + $spec = $this->product->getSpecialProductTypeObject(); + $spec->setTaxClass($product->getMwert() * 100); } elseif ($this->product->getUuid()) { /** * @var \PSC\Shop\EntityBundle\Entity\Product $product @@ -169,6 +171,9 @@ class Producer implements IUiProducer, IProducerHydrateModel, ICalcNeedContact $engine->loadString($product->getCalcXml()); $engine->setFormulas($product->getShop()->getFormel()); $engine->setParameters($product->getShop()->getParameter()); + $spec = $this->product->getSpecialProductTypeObject(); + + $spec->setTaxClass($product->getMwert() * 100); } $this->engine = $engine; @@ -248,7 +253,7 @@ class Producer implements IUiProducer, IProducerHydrateModel, ICalcNeedContact return $temp; } - public function setContact(null|Contact $contact): void + public function setContact(?Contact $contact): void { $this->contact = $contact; }