feat: moneta 2.0, MONETA.Assistant payment module for PHPShop 6.7+
This commit is contained in:
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace MonetaPhpShop\receipt;
|
||||
|
||||
use Closure;
|
||||
|
||||
/**
|
||||
* Åäèíèöà èçìåðåíèÿ ïîçèöèè ÷åêà (`measure`, ïåðå÷åíü `kassaspecification.pdf`) ïî åäèíèöå òîâàðà
|
||||
* PHPShop (`ed_izm` — ñâîáîäíûé òåêñò: «øò.», «êã», «ì2»). Âàðèàíòû íàïèñàíèÿ — òå, ÷òî êàññà
|
||||
* ðàñïîçíà¸ò ñàìà («àâòîîïðåäåëåíèå» â ñïåöèôèêàöèè), ïëþñ òî÷êà íà êîíöå è ïðîáåëû, êàê ïèøóò
|
||||
* â PHPShop. Íå ðàñïîçíàíî èëè ïóñòî — `unit` (øòóêà), çíà÷åíèå êàññû ïî óìîë÷àíèþ.
|
||||
*
|
||||
* Ñðàâíåíèå áåç ó÷¸òà ðåãèñòðà ÷åðåç `/iu`: `mbstring` ÿäðî PHPShop íå òðåáóåò. Âàðèàíòû — ëèòåðàëû
|
||||
* ìîäóëÿ: ïîñëå ñáîðêè îíè â cp1251, à øàáëîí ñ `/u` îáÿçàí áûòü UTF-8, ïîýòîìó ïåðåä ñðàâíåíèåì èõ
|
||||
* ïåðåêîäèðóåò òîò æå `$toUtf8`, ÷òî è ïðî÷èå ëèòåðàëû ÷åêà ({@see ReceiptBuilder}).
|
||||
*/
|
||||
final class Measure
|
||||
{
|
||||
public const UNIT = 'unit';
|
||||
|
||||
/** Êîä êàññû -> âàðèàíòû íàïèñàíèÿ (áåç ïðîáåëîâ, â íèæíåì ðåãèñòðå). */
|
||||
private const VARIANTS = [
|
||||
self::UNIT => ['øòóêà', 'øòóêè', 'øòóê', 'øò', 'pc', 'pce', 'pcs', 'nmb'],
|
||||
'gram' => ['ãðàìì', 'ãðàììû', 'ã', 'ãð', 'g', 'grm'],
|
||||
'kilogram' => ['êèëîãðàìì', 'êèëîãðàììû', 'êã', 'kg', 'kgm'],
|
||||
'ton' => ['òîííà', 'òîííû', 'ò', 't', 'tne'],
|
||||
'centimeter' => ['ñàíòèìåòð', 'ñì', 'cm', 'cmt'],
|
||||
'decimeter' => ['äåöèìåòð', 'äì', 'dm', 'dmt'],
|
||||
'meter' => ['ìåòð', 'ìåòðû', 'ì', 'm', 'mtr', 'ïì', 'ïîã.ì'],
|
||||
'square_centimeter' => ['êâàäðàòíûéñàíòèìåòð', 'êâ.ñì', 'ñì2', 'cm2', 'cmk'],
|
||||
'square_decimeter' => ['êâàäðàòíûéäåöèìåòð', 'êâ.äì', 'äì2', 'dm2', 'dmk'],
|
||||
'square_meter' => ['êâàäðàòíûéìåòð', 'êâ.ì', 'ì2', 'm2', 'mtk'],
|
||||
'milliliter' => ['ìèëëèëèòð', 'ìë', 'ml', 'mlt'],
|
||||
'liter' => ['ëèòð', 'ëèòðû', 'ë', 'l', 'ltr'],
|
||||
'cubic_meter' => ['êóáè÷åñêèéìåòð', 'êóá.ì', 'ì3', 'm3', 'mtq'],
|
||||
'kilowatt_hour' => ['êèëîâàòò÷àñ', 'êâò*÷', 'êâò-÷', 'êâò÷', 'kwh'],
|
||||
'gigacalorie' => ['ãèãàêàëîðèÿ', 'ãêàë', 'ãèãàêàë'],
|
||||
'day' => ['ñóòêè', 'ñóò', 'äåíü', 'äí', 'd', 'day'],
|
||||
'hour' => ['÷àñ', '÷', 'h', 'hur'],
|
||||
'minute' => ['ìèíóòà', 'ìèí', 'min'],
|
||||
'second' => ['ñåêóíäà', 'ñåê', 'ñ', 's', 'sec'],
|
||||
'kilobyte' => ['êèëîáàéò', 'êáàéò', 'êá', 'kib', 'êèá', 'kb'],
|
||||
'megabyte' => ['ìåãàáàéò', 'ìáàéò', 'ìá', 'mib', 'ìèá', 'mb'],
|
||||
'gigabyte' => ['ãèãàáàéò', 'ãáàéò', 'ãá', 'gib', 'ãèá', 'gb'],
|
||||
'terabyte' => ['òåðàáàéò', 'òáàéò', 'òá', 'tib', 'òèá', 'tb'],
|
||||
];
|
||||
|
||||
private function __construct() {}
|
||||
|
||||
/**
|
||||
* @param string $unit åäèíèöà òîâàðà â UTF-8
|
||||
* @param Closure(string): string $toUtf8 ëèòåðàëû ìîäóëÿ -> UTF-8
|
||||
*/
|
||||
public static function fromUnit(string $unit, Closure $toUtf8): string
|
||||
{
|
||||
// Ïðîáåëû íå çíà÷èìû («êâ. ì»), òî÷êà íà êîíöå — ñîêðàùåíèå («øò.»).
|
||||
$unit = rtrim((string) preg_replace('/\s+/u', '', $unit), '.');
|
||||
if ($unit === '') {
|
||||
return self::UNIT;
|
||||
}
|
||||
foreach (self::VARIANTS as $measure => $variants) {
|
||||
foreach ($variants as $variant) {
|
||||
if (preg_match('/\A' . preg_quote($toUtf8($variant), '/') . '\z/iu', $unit) === 1) {
|
||||
return $measure;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return self::UNIT;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace MonetaPhpShop\receipt;
|
||||
|
||||
/**
|
||||
* Ïðèçíàê ñïîñîáà ðàñ÷¸òà (`pm` â `INVENTORY`, `paymentMethod` â JSON Check URL): âåñü ïåðå÷åíü
|
||||
* `kassaspecification.pdf`. Äåéñòâóåò íà âñå ïîçèöèè ÷åêà, âêëþ÷àÿ äîñòàâêó.
|
||||
* Ðàñ÷¸òíûå ñòàâêè ÍÄÑ ïðè àâàíñå è ïðåäîïëàòå ìîäóëü ñàì íå âûáèðàåò.
|
||||
* Çíà÷åíèÿ õðàíÿòñÿ â íàñòðîéêàõ ìîäóëÿ — íå ïåðåèìåíîâûâàòü.
|
||||
*/
|
||||
final class PaymentMethod
|
||||
{
|
||||
public const FULL_PREPAYMENT = 'full_prepayment';
|
||||
public const PREPAYMENT = 'prepayment';
|
||||
public const ADVANCE = 'advance';
|
||||
public const FULL_PAYMENT = 'full_payment';
|
||||
public const PARTIAL_PAYMENT = 'partial_payment';
|
||||
public const CREDIT = 'credit';
|
||||
public const CREDIT_PAYMENT = 'credit_payment';
|
||||
|
||||
/** Çíà÷åíèå -> ïîäïèñü â íàñòðîéêàõ, ïîðÿäîê — êàê â `kassaspecification.pdf`. */
|
||||
public const LABELS = [
|
||||
self::FULL_PREPAYMENT => 'Ïîëíàÿ ïðåäîïëàòà',
|
||||
self::PREPAYMENT => '×àñòè÷íàÿ ïðåäîïëàòà',
|
||||
self::ADVANCE => 'Àâàíñ',
|
||||
self::FULL_PAYMENT => 'Ïîëíûé ðàñ÷¸ò',
|
||||
self::PARTIAL_PAYMENT => '×àñòè÷íûé ðàñ÷¸ò è êðåäèò',
|
||||
self::CREDIT => 'Êðåäèò',
|
||||
self::CREDIT_PAYMENT => 'Âûïëàòà ïî êðåäèòó',
|
||||
];
|
||||
|
||||
/** @var string */
|
||||
private $value;
|
||||
|
||||
private function __construct(string $value)
|
||||
{
|
||||
$this->value = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Çíà÷åíèå èç íàñòðîåê; íåèçâåñòíîå èëè ïóñòîå — ïîëíûé ðàñ÷¸ò (äåôîëò êàññû).
|
||||
*/
|
||||
public static function fromSetting(?string $value): self
|
||||
{
|
||||
return new self(isset(self::LABELS[(string) $value]) ? (string) $value : self::FULL_PAYMENT);
|
||||
}
|
||||
|
||||
public static function fullPayment(): self
|
||||
{
|
||||
return new self(self::FULL_PAYMENT);
|
||||
}
|
||||
|
||||
public function getValue(): string
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace MonetaPhpShop\receipt;
|
||||
|
||||
/**
|
||||
* Ïðèçíàê ïðåäìåòà ðàñ÷¸òà (`po` â `INVENTORY`, `paymentObject` â JSON Check URL): ïåðå÷åíü
|
||||
* `kassaspecification.pdf` ïëþñ `composite`.
|
||||
* Íàñòðîéêîé âûáèðàåòñÿ òîëüêî äëÿ òîâàðîâ; äîñòàâêà âñåãäà {@see self::SERVICE}.
|
||||
* Çíà÷åíèÿ õðàíÿòñÿ â íàñòðîéêàõ ìîäóëÿ — íå ïåðåèìåíîâûâàòü.
|
||||
*/
|
||||
final class PaymentObject
|
||||
{
|
||||
public const COMMODITY = 'commodity';
|
||||
public const SERVICE = 'service';
|
||||
|
||||
/** Çíà÷åíèå -> ïîäïèñü â íàñòðîéêàõ. */
|
||||
public const LABELS = [
|
||||
self::COMMODITY => 'Òîâàð',
|
||||
'excise' => 'Ïîäàêöèçíûé òîâàð',
|
||||
'job' => 'Ðàáîòà',
|
||||
self::SERVICE => 'Óñëóãà',
|
||||
'gambling_bet' => 'Ñòàâêà àçàðòíîé èãðû',
|
||||
'gambling_prize' => 'Âûèãðûø àçàðòíîé èãðû',
|
||||
'lottery' => 'Ëîòåðåéíûé áèëåò',
|
||||
'lottery_prize' => 'Âûèãðûø ëîòåðåè',
|
||||
'intellectual_activity' => 'Ïðåäîñòàâëåíèå ðåçóëüòàòîâ èíòåëëåêòóàëüíîé äåÿòåëüíîñòè',
|
||||
'payment' => 'Ïëàò¸æ',
|
||||
'agent_commission' => 'Àãåíòñêîå âîçíàãðàæäåíèå',
|
||||
'award' => 'Âûïëàòà (âçíîñ, ïåíè, øòðàô, âîçíàãðàæäåíèå, áîíóñ)',
|
||||
'composite' => 'Ñîñòàâíîé ïðåäìåò ðàñ÷¸òà',
|
||||
'another' => 'Äðóãîå',
|
||||
'property_right' => 'Èìóùåñòâåííîå ïðàâî',
|
||||
'non_operating' => 'Âíåðåàëèçàöèîííûé äîõîä',
|
||||
'insurance_premium' => 'Ñòðàõîâîé ñáîð',
|
||||
'sales_tax' => 'Òîðãîâûé ñáîð',
|
||||
'resort_fee' => 'Êóðîðòíûé ñáîð',
|
||||
];
|
||||
|
||||
/** @var string */
|
||||
private $value;
|
||||
|
||||
private function __construct(string $value)
|
||||
{
|
||||
$this->value = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Çíà÷åíèå èç íàñòðîåê; íåèçâåñòíîå èëè ïóñòîå — òîâàð (äåôîëò êàññû).
|
||||
*/
|
||||
public static function fromSetting(?string $value): self
|
||||
{
|
||||
return new self(isset(self::LABELS[(string) $value]) ? (string) $value : self::COMMODITY);
|
||||
}
|
||||
|
||||
public static function commodity(): self
|
||||
{
|
||||
return new self(self::COMMODITY);
|
||||
}
|
||||
|
||||
public static function service(): self
|
||||
{
|
||||
return new self(self::SERVICE);
|
||||
}
|
||||
|
||||
public function getValue(): string
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace MonetaPhpShop\receipt;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use MonetaPhpShop\Money;
|
||||
|
||||
/**
|
||||
* Êîëè÷åñòâî ïîçèöèè ÷åêà ñ òî÷íîñòüþ êàññû — òðè çíàêà ïîñëå òî÷êè (`kassaspecification.pdf`,
|
||||
* â ñõåìå Check URL `multipleOf 0.001`). Õðàíèòñÿ öåëûì ÷èñëîì òûñÿ÷íûõ, êàê êîïåéêè â {@see Money}:
|
||||
* Êîðçèíà ÿäðà õðàíèò êîëè÷åñòâî êàê åñòü.
|
||||
*/
|
||||
final class Quantity
|
||||
{
|
||||
private const SCALE = 1000;
|
||||
|
||||
/** Ïîòîëîê êàññû: 99999.999. */
|
||||
public const MAX_THOUSANDTHS = 99999999;
|
||||
|
||||
/** @var int */
|
||||
private $thousandths;
|
||||
|
||||
private function __construct(int $thousandths)
|
||||
{
|
||||
if (($thousandths < 1) || ($thousandths > self::MAX_THOUSANDTHS)) {
|
||||
throw new InvalidArgumentException('Êîëè÷åñòâî ïîçèöèè ÷åêà âíå ãðàíèö êàññû.');
|
||||
}
|
||||
|
||||
$this->thousandths = $thousandths;
|
||||
}
|
||||
|
||||
public static function pieces(int $pieces): self
|
||||
{
|
||||
return new self($pieces * self::SCALE);
|
||||
}
|
||||
|
||||
public static function fromThousandths(int $thousandths): self
|
||||
{
|
||||
return new self($thousandths);
|
||||
}
|
||||
|
||||
/**
|
||||
* Êîëè÷åñòâî èç êîðçèíû çàêàçà (`num`): ÷èñëî èëè ÷èñëîâàÿ ñòðîêà, îêðóãëÿåòñÿ äî òûñÿ÷íûõ.
|
||||
* Íîëü, îòðèöàòåëüíîå, íå ÷èñëî èëè áîëüøå ïîòîëêà êàññû — null.
|
||||
*
|
||||
* @param mixed $raw
|
||||
*/
|
||||
public static function fromCart($raw): ?self
|
||||
{
|
||||
if (is_string($raw)) {
|
||||
$raw = trim($raw);
|
||||
}
|
||||
if (!is_int($raw) && !is_float($raw) && !(is_string($raw) && is_numeric($raw))) {
|
||||
return null;
|
||||
}
|
||||
$thousandths = (int) round((float) $raw * self::SCALE);
|
||||
if (($thousandths < 1) || ($thousandths > self::MAX_THOUSANDTHS)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new self($thousandths);
|
||||
}
|
||||
|
||||
public function getThousandths(): int
|
||||
{
|
||||
return $this->thousandths;
|
||||
}
|
||||
|
||||
/**
|
||||
* Öåëîå ëè êîëè÷åñòâî: òîëüêî òàêóþ ñòðîêó ìîæíî ðåçàòü íà äâå ñ ðàçíèöåé öåí â êîïåéêó.
|
||||
*/
|
||||
public function isWhole(): bool
|
||||
{
|
||||
return $this->thousandths % self::SCALE === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Öåëîå ÷èñëî øòóê; òîëüêî äëÿ {@see self::isWhole()}.
|
||||
*/
|
||||
public function getPieces(): int
|
||||
{
|
||||
return intdiv($this->thousandths, self::SCALE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Äåñÿòè÷íàÿ çàïèñü áåç ëèøíèõ íóëåé: `2`, `2.5`, `0.125`.
|
||||
*/
|
||||
public function toDecimal(): string
|
||||
{
|
||||
if ($this->isWhole()) {
|
||||
return (string) $this->getPieces();
|
||||
}
|
||||
|
||||
return rtrim(
|
||||
sprintf('%d.%03d', intdiv($this->thousandths, self::SCALE), $this->thousandths % self::SCALE),
|
||||
'0',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ñóììà ïîçèöèè: öåíà x êîëè÷åñòâî, îêðóãëåíèå äî êîïåéêè (ïîëîâèíà — ââåðõ).
|
||||
*/
|
||||
public function totalOf(Money $price): Money
|
||||
{
|
||||
return Money::fromMinorUnits(
|
||||
intdiv(($price->toMinorUnits() * $this->thousandths) + intdiv(self::SCALE, 2), self::SCALE),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace MonetaPhpShop\receipt;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use MonetaPhpShop\Money;
|
||||
use MonetaPhpShop\protocol\JsonNumber;
|
||||
|
||||
/**
|
||||
* ×åê 54-ÔÇ çàêàçà: ïîêóïàòåëü è ïîçèöèè, ñóììà ïîçèöèé ðàâíà ñóììå îïëàòû äî êîïåéêè.
|
||||
* Îòäà¸òñÿ â äâóõ ôîðìàõ: `MNT_ATTRIBUTES` XML-îòâåòîâ è îáúåêò `receipt` JSON-îòâåòà Check URL.
|
||||
*/
|
||||
final class Receipt
|
||||
{
|
||||
/** @var ReceiptClient|null */
|
||||
private $client;
|
||||
|
||||
/** @var array<int, ReceiptItem> */
|
||||
private $items;
|
||||
|
||||
/**
|
||||
* @param array<int, ReceiptItem> $items
|
||||
*/
|
||||
public function __construct(?ReceiptClient $client, array $items)
|
||||
{
|
||||
if ($items === []) {
|
||||
throw new InvalidArgumentException('×åê áåç ïîçèöèé.');
|
||||
}
|
||||
|
||||
$this->client = $client;
|
||||
$this->items = array_values($items);
|
||||
}
|
||||
|
||||
public function getClient(): ?ReceiptClient
|
||||
{
|
||||
return $this->client;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, ReceiptItem>
|
||||
*/
|
||||
public function getItems(): array
|
||||
{
|
||||
return $this->items;
|
||||
}
|
||||
|
||||
public function total(): Money
|
||||
{
|
||||
$total = Money::fromMinorUnits(0);
|
||||
foreach ($this->items as $item) {
|
||||
$total = $total->add($item->total());
|
||||
}
|
||||
|
||||
return $total;
|
||||
}
|
||||
|
||||
/**
|
||||
* `MNT_ATTRIBUTES`: `INVENTORY` âñåãäà; `CLIENT`, `CUSTOMER` (email), `PHONE` (áåç `+`) —
|
||||
* åñëè åñòü êîíòàêò; `SNO` — ïðè ôèñêàëèçàöèè, åñëè ñèñòåìà íàëîãîîáëîæåíèÿ âûáðàíà.
|
||||
* `DELIVERY` íå ïåðåäà¸òñÿ: äîñòàâêà — ïîçèöèåé `INVENTORY`.
|
||||
* Áåç ôèñêàëèçàöèè ïîçèöèè óðåçàíû ({@see ReceiptItem::toInventoryPosition()}).
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function toAttributes(bool $fiscalization = true, ?TaxSystem $taxSystem = null): array
|
||||
{
|
||||
$attributes = [
|
||||
'INVENTORY' => JsonNumber::encode(
|
||||
array_map(static function (ReceiptItem $item) use ($fiscalization): array {
|
||||
return $item->toInventoryPosition($fiscalization);
|
||||
}, $this->items),
|
||||
),
|
||||
];
|
||||
|
||||
$client = ($this->client !== null) ? $this->client->toArray(ReceiptClient::KASSA_PHONE_MAX_LENGTH) : [];
|
||||
if ($client !== []) {
|
||||
$attributes['CLIENT'] = JsonNumber::encode([$client]);
|
||||
if (isset($client['email'])) {
|
||||
$attributes['CUSTOMER'] = $client['email'];
|
||||
}
|
||||
if (isset($client['phone'])) {
|
||||
$attributes['PHONE'] = ltrim($client['phone'], '+');
|
||||
}
|
||||
}
|
||||
|
||||
if ($fiscalization && ($taxSystem !== null) && $taxSystem->isPassed()) {
|
||||
$attributes['SNO'] = $taxSystem->getValue();
|
||||
}
|
||||
|
||||
return $attributes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Îáúåêò `receipt` JSON-îòâåòà Check URL. Áåç êîíòàêòà — ïóñòîé `client` (`{}`).
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toJsonReceipt(): array
|
||||
{
|
||||
$client = $this->client !== null ? $this->client->toArray(ReceiptClient::JSON_PHONE_MAX_LENGTH) : [];
|
||||
|
||||
return [
|
||||
'client' => $client === [] ? new \stdClass() : $client,
|
||||
'items' => array_map(static function (ReceiptItem $item): array {
|
||||
return $item->toCheckItem();
|
||||
}, $this->items),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,360 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace MonetaPhpShop\receipt;
|
||||
|
||||
use Closure;
|
||||
use InvalidArgumentException;
|
||||
use MonetaPhpShop\Money;
|
||||
use MonetaPhpShop\order\Order;
|
||||
|
||||
/**
|
||||
* ×åê çàêàçà PHPShop: ñòðîêè êîðçèíû, äîñòàâêà îòäåëüíîé ïîçèöèåé, ïîêóïàòåëü.
|
||||
*
|
||||
* Ñóììà ïîçèöèé îáÿçàíà ñîâïàñòü ñ ñóììîé çàêàçà äî êîïåéêè (`cmsspecification.pdf`), à èòîã
|
||||
* PHPShop (`phpshop_orders.sum`) — ýòî òîâàðû ñî ñêèäêîé (êðîìå ïðîìî-öåí), ìèíóñ áîíóñû, ïëþñ
|
||||
* äîñòàâêà öåëûì ÷èñëîì, ñ îêðóãëåíèåì ÿäðà. Ïîýòîìó öåíû ñòðîê ñíà÷àëà ñ÷èòàþòñÿ êàê ó ÿäðà,
|
||||
* à ðàñõîæäåíèå (áîíóñû, îêðóãëåíèÿ, ðó÷íàÿ ïðàâêà çàêàçà) ðàçíîñèòñÿ ïî òîâàðíûì ñòðîêàì
|
||||
* ïðîïîðöèîíàëüíî èõ ñóììå; øòó÷íàÿ ñòðîêà, êîòîðàÿ íå äåëèòñÿ íà êîëè÷åñòâî, ðåæåòñÿ íà äâå
|
||||
* ñ ðàçíèöåé öåí â êîïåéêó, ó âåñîâîé (äðîáíîå êîëè÷åñòâî) ïîäáèðàåòñÿ öåíà çà åäèíèöó. Íå ïîëó÷èëîñü —
|
||||
* îäíà ïîçèöèÿ «Îïëàòà çàêàçà» íà âñþ ñóììó.
|
||||
*/
|
||||
final class ReceiptBuilder
|
||||
{
|
||||
private const DELIVERY_NAME = 'Äîñòàâêà';
|
||||
private const ITEM_FALLBACK_NAME = 'Òîâàð';
|
||||
private const PAYMENT_NAME = 'Îïëàòà çàêàçà ¹';
|
||||
|
||||
/** @var Closure(string): string */
|
||||
private $toUtf8;
|
||||
|
||||
/** @var PaymentMethod */
|
||||
private $paymentMethod;
|
||||
|
||||
/** @var PaymentObject */
|
||||
private $goodsObject;
|
||||
|
||||
/** @var Vat */
|
||||
private $paymentVat;
|
||||
|
||||
/**
|
||||
* @param Closure(string): string $toUtf8 ñòðîêè çàêàçà è ëèòåðàëû ìîäóëÿ (cp1251 ïîñëå ñáîðêè) -> UTF-8
|
||||
* @param PaymentMethod $paymentMethod ñïîñîá ðàñ÷¸òà âñåõ ïîçèöèé — íàñòðîéêà ìîäóëÿ
|
||||
* @param PaymentObject|null $goodsObject ïðåäìåò ðàñ÷¸òà òîâàðîâ — íàñòðîéêà ìîäóëÿ; null — òîâàð.
|
||||
* Ó äîñòàâêè âñåãäà óñëóãà.
|
||||
* @param Vat|null $paymentVat ñòàâêà çàïàñíîé ïîçèöèè «Îïëàòà çàêàçà»: ôèêñèðîâàííàÿ «ÍÄÑ íà òîâàðû»;
|
||||
* null — áåç ÍÄÑ
|
||||
*/
|
||||
public function __construct(
|
||||
Closure $toUtf8,
|
||||
PaymentMethod $paymentMethod,
|
||||
?PaymentObject $goodsObject = null,
|
||||
?Vat $paymentVat = null
|
||||
) {
|
||||
$this->toUtf8 = $toUtf8;
|
||||
$this->paymentMethod = $paymentMethod;
|
||||
$this->goodsObject = $goodsObject ?? PaymentObject::commodity();
|
||||
$this->paymentVat = $paymentVat ?? Vat::none();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Vat $goodsVat ñèñòåìíàÿ ñòàâêà PHPShop äëÿ òîâàðîâ
|
||||
* @param Vat $deliveryVat ñòàâêà ñïîñîáà äîñòàâêè
|
||||
* @param string $fallbackEmail íàñòðîéêà «Email äëÿ ÷åêîâ», ìîæåò áûòü ïóñòîé
|
||||
*/
|
||||
public function build(Order $order, Vat $goodsVat, Vat $deliveryVat, string $fallbackEmail): Receipt
|
||||
{
|
||||
$client = ReceiptClient::fromContacts(
|
||||
$order->getEmail(),
|
||||
$order->getPhone(),
|
||||
$this->utf8($order->getCustomerName()),
|
||||
$fallbackEmail,
|
||||
);
|
||||
|
||||
$amount = $order->getAmount()->toMinorUnits();
|
||||
|
||||
$items = $this->items($this->balance($this->lines($order, $goodsVat, $deliveryVat), $amount));
|
||||
if ($items === null) {
|
||||
$items = [
|
||||
new ReceiptItem(
|
||||
$this->utf8(self::PAYMENT_NAME . $order->getUid()),
|
||||
$order->getAmount(),
|
||||
Quantity::pieces(1),
|
||||
$this->paymentVat,
|
||||
$this->goodsObject,
|
||||
$this->paymentMethod,
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
return new Receipt($client, $items);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ñòðîêè ÷åêà ïî ïðàâèëàì ÿäðà (`PHPShopDone::send_to_order()`): ñêèäêà ïîêóïàòåëÿ — êî âñåì
|
||||
* òîâàðàì áåç ïðîìî-öåíû, äîñòàâêà èç êîðçèíû çàêàçà. Êîëè÷åñòâî — êàê â êîðçèíå, â òîì ÷èñëå
|
||||
* äðîáíîå (âåñîâîé òîâàð), åäèíèöà èçìåðåíèÿ — èç `ed_izm` òîâàðà.
|
||||
*
|
||||
* @return array<int, array{name: string, unit: int, quantity: Quantity, measure: string, vat: Vat, delivery: bool}>
|
||||
*/
|
||||
private function lines(Order $order, Vat $goodsVat, Vat $deliveryVat): array
|
||||
{
|
||||
$cart = $order->getCart();
|
||||
$lines = [];
|
||||
$products = isset($cart['cart']) && is_array($cart['cart']) ? $cart['cart'] : [];
|
||||
$discount = $order->getDiscountPercent();
|
||||
foreach ($products as $product) {
|
||||
if (!is_array($product)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$price = (float) ($product['price'] ?? 0);
|
||||
if (($discount > 0) && empty($product['promo_price'])) {
|
||||
$price -= $price * $discount / 100;
|
||||
}
|
||||
|
||||
$quantity = Quantity::fromCart($product['num'] ?? null);
|
||||
if (($quantity === null) || ($price <= 0)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$lines[] = [
|
||||
'name' => $this->name((string) ($product['name'] ?? '')),
|
||||
'unit' => Money::fromFloat($price)->toMinorUnits(),
|
||||
'quantity' => $quantity,
|
||||
'measure' => Measure::fromUnit($this->utf8((string) ($product['ed_izm'] ?? '')), $this->toUtf8),
|
||||
'vat' => $goodsVat,
|
||||
'delivery' => false,
|
||||
];
|
||||
}
|
||||
|
||||
$delivery = (float) ($cart['dostavka'] ?? 0);
|
||||
if ($delivery > 0) {
|
||||
$lines[] = [
|
||||
'name' => $this->utf8(self::DELIVERY_NAME),
|
||||
'unit' => Money::fromFloat($delivery)->toMinorUnits(),
|
||||
'quantity' => Quantity::pieces(1),
|
||||
'measure' => Measure::UNIT,
|
||||
'vat' => $deliveryVat,
|
||||
'delivery' => true,
|
||||
];
|
||||
}
|
||||
|
||||
return $lines;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ïîäãîíêà ñóììû ñòðîê ïîä ñóììó çàêàçà â êîïåéêàõ. Ñíà÷àëà ðàñõîæäåíèå áåðóò øòó÷íûå òîâàðû;
|
||||
* åñëè íå óêëàäûâàåòñÿ — âñå øòó÷íûå ñòðîêè âìåñòå ñ äîñòàâêîé; åñëè øòó÷íûõ ñòðîê íåò —
|
||||
* è âåñîâûå, ïîäáîðîì öåíû çà åäèíèöó. Ñòðîêó âåñîâîãî òîâàðà íà äâå íå ðàçðåçàòü, ïîýòîìó
|
||||
* å¸ òðîãàþò ïîñëåäíåé. Ïóñòîé ðåçóëüòàò — íå âûøëî.
|
||||
*
|
||||
* @param array<int, array{name: string, unit: int, quantity: Quantity, measure: string, vat: Vat, delivery: bool}> $lines
|
||||
*
|
||||
* @return array<int, array{name: string, unit: int, quantity: Quantity, measure: string, vat: Vat, delivery: bool}>
|
||||
*/
|
||||
private function balance(array $lines, int $target): array
|
||||
{
|
||||
if (($target <= 0) || ($lines === [])) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$totals = [];
|
||||
foreach ($lines as $index => $line) {
|
||||
$totals[$index] = self::lineTotal($line);
|
||||
}
|
||||
if (array_sum($totals) === $target) {
|
||||
return $lines;
|
||||
}
|
||||
|
||||
$stages = [
|
||||
static function (array $line): bool {
|
||||
return !$line['delivery'] && $line['quantity']->isWhole();
|
||||
},
|
||||
static function (array $line): bool {
|
||||
return $line['quantity']->isWhole();
|
||||
},
|
||||
static function (array $line): bool {
|
||||
return true;
|
||||
},
|
||||
];
|
||||
foreach ($stages as $isAdjustable) {
|
||||
$adjustable = [];
|
||||
$fixed = 0;
|
||||
foreach ($lines as $index => $line) {
|
||||
if ($isAdjustable($line)) {
|
||||
$adjustable[] = $index;
|
||||
} else {
|
||||
$fixed += $totals[$index];
|
||||
}
|
||||
}
|
||||
if (($adjustable === []) || ($target - $fixed <= 0)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
return $this->distribute($lines, $totals, $adjustable, $target - $fixed);
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Ðàçíîñ ñóììû `$goal` ïî ñòðîêàì `$adjustable` ïðîïîðöèîíàëüíî èõ ñóììå.
|
||||
*
|
||||
* @param array<int, array{name: string, unit: int, quantity: Quantity, measure: string, vat: Vat, delivery: bool}> $lines
|
||||
* @param array<int, int> $totals ñóììû ñòðîê â êîïåéêàõ
|
||||
* @param array<int, int> $adjustable èíäåêñû ñòðîê, êîòîðûå ìîæíî ìåíÿòü
|
||||
*
|
||||
* @return array<int, array{name: string, unit: int, quantity: Quantity, measure: string, vat: Vat, delivery: bool}>
|
||||
*/
|
||||
private function distribute(array $lines, array $totals, array $adjustable, int $goal): array
|
||||
{
|
||||
$base = 0;
|
||||
foreach ($adjustable as $index) {
|
||||
$base += $totals[$index];
|
||||
}
|
||||
if ($base <= 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Íàèáîëüøèå îñòàòêè: äîëè îêðóãëÿþòñÿ âíèç, íåäîñòàþùèå êîïåéêè — ñòðîêàì ñ áîëüøèì îñòàòêîì.
|
||||
$shares = [];
|
||||
$remainders = [];
|
||||
foreach ($adjustable as $index) {
|
||||
if ($totals[$index] > intdiv(PHP_INT_MAX, $goal)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$product = $totals[$index] * $goal;
|
||||
$shares[$index] = intdiv($product, $base);
|
||||
$remainders[$index] = $product % $base;
|
||||
}
|
||||
arsort($remainders);
|
||||
$left = $goal - array_sum($shares);
|
||||
foreach (array_keys($remainders) as $index) {
|
||||
if ($left <= 0) {
|
||||
break;
|
||||
}
|
||||
$shares[$index]++;
|
||||
$left--;
|
||||
}
|
||||
|
||||
$balanced = [];
|
||||
foreach ($lines as $index => $line) {
|
||||
if (!isset($shares[$index])) {
|
||||
$balanced[] = $line;
|
||||
continue;
|
||||
}
|
||||
|
||||
$parts = $line['quantity']->isWhole()
|
||||
? $this->split($line, $shares[$index])
|
||||
: $this->reprice($line, $shares[$index]);
|
||||
if ($parts === []) {
|
||||
return [];
|
||||
}
|
||||
foreach ($parts as $part) {
|
||||
$balanced[] = $part;
|
||||
}
|
||||
}
|
||||
|
||||
return $balanced;
|
||||
}
|
||||
|
||||
/**
|
||||
* Øòó÷íàÿ ñòðîêà ñ íîâîé ñóììîé: öåëàÿ öåíà çà øòóêó, îñòàòîê êîïååê — âòîðîé ñòðîêîé òîé æå
|
||||
* ïîçèöèè ñ öåíîé íà êîïåéêó âûøå. Íóëåâûå ÷àñòè îòáðàñûâàþòñÿ.
|
||||
*
|
||||
* @param array{name: string, unit: int, quantity: Quantity, measure: string, vat: Vat, delivery: bool} $line
|
||||
*
|
||||
* @return array<int, array{name: string, unit: int, quantity: Quantity, measure: string, vat: Vat, delivery: bool}>
|
||||
*/
|
||||
private function split(array $line, int $total): array
|
||||
{
|
||||
$pieces = $line['quantity']->getPieces();
|
||||
$unit = intdiv($total, $pieces);
|
||||
$rest = $total - ($unit * $pieces);
|
||||
|
||||
$parts = [];
|
||||
if ($rest > 0) {
|
||||
$parts[] = ['unit' => $unit + 1, 'quantity' => Quantity::pieces($rest)] + $line;
|
||||
}
|
||||
if (($unit > 0) && ($pieces - $rest > 0)) {
|
||||
$parts[] = ['unit' => $unit, 'quantity' => Quantity::pieces($pieces - $rest)] + $line;
|
||||
}
|
||||
|
||||
return $parts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Âåñîâàÿ ñòðîêà ñ íîâîé ñóììîé: öåíà çà åäèíèöó, ïðè êîòîðîé öåíà x êîëè÷åñòâî äî êîïåéêè
|
||||
* äà¸ò ðîâíî `$total`. Òàêîé öåíû ìîæåò íå áûòü (íà 2.5 êã øàã ñóììû — 2–3 êîïåéêè) — òîãäà ïóñòî.
|
||||
*
|
||||
* @param array{name: string, unit: int, quantity: Quantity, measure: string, vat: Vat, delivery: bool} $line
|
||||
*
|
||||
* @return array<int, array{name: string, unit: int, quantity: Quantity, measure: string, vat: Vat, delivery: bool}>
|
||||
*/
|
||||
private function reprice(array $line, int $total): array
|
||||
{
|
||||
$thousandths = $line['quantity']->getThousandths();
|
||||
$estimate = intdiv(($total * 1000) + intdiv($thousandths, 2), $thousandths);
|
||||
foreach ([$estimate, $estimate - 1, $estimate + 1] as $unit) {
|
||||
if ($unit > 0 && self::lineTotal(['unit' => $unit] + $line) === $total) {
|
||||
return [['unit' => $unit] + $line];
|
||||
}
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array{unit: int, quantity: Quantity} $line
|
||||
*/
|
||||
private static function lineTotal(array $line): int
|
||||
{
|
||||
return $line['quantity']->totalOf(Money::fromMinorUnits($line['unit']))->toMinorUnits();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, array{name: string, unit: int, quantity: Quantity, measure: string, vat: Vat, delivery: bool}> $lines
|
||||
*
|
||||
* @return array<int, ReceiptItem>|null null — ñòðîê íåò èëè ñòðîêà íå ïðîõîäèò îãðàíè÷åíèÿ êàññû
|
||||
*/
|
||||
private function items(array $lines): ?array
|
||||
{
|
||||
if ($lines === []) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$items = [];
|
||||
try {
|
||||
foreach ($lines as $line) {
|
||||
$items[] = new ReceiptItem(
|
||||
$line['name'],
|
||||
Money::fromMinorUnits($line['unit']),
|
||||
$line['quantity'],
|
||||
$line['vat'],
|
||||
$line['delivery'] ? PaymentObject::service() : $this->goodsObject,
|
||||
$this->paymentMethod,
|
||||
$line['measure'],
|
||||
);
|
||||
}
|
||||
} catch (InvalidArgumentException $exception) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $items;
|
||||
}
|
||||
|
||||
private function name(string $raw): string
|
||||
{
|
||||
$name = ReceiptText::clean($this->utf8($raw), ReceiptItem::NAME_MAX_LENGTH);
|
||||
|
||||
return ($name !== '') ? $name : $this->utf8(self::ITEM_FALLBACK_NAME);
|
||||
}
|
||||
|
||||
private function utf8(string $value): string
|
||||
{
|
||||
return ($this->toUtf8)($value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace MonetaPhpShop\receipt;
|
||||
|
||||
use MonetaPhpShop\PhoneNumber;
|
||||
|
||||
/**
|
||||
* Ïîêóïàòåëü â ÷åêå — îáúåêò `client` (`kassaspecification.pdf`): íóæåí õîòÿ áû îäèí èç
|
||||
* `email`/`phone`, ÔÈÎ — òîëüêî âìåñòå ñ íèìè. Ñòðîêè â UTF-8.
|
||||
*/
|
||||
final class ReceiptClient
|
||||
{
|
||||
public const NAME_MAX_LENGTH = 256;
|
||||
public const EMAIL_MAX_LENGTH = 64;
|
||||
|
||||
/** `phone_number` JSON-ñõåìû Check URL (`cmsspecification.pdf`). */
|
||||
public const JSON_PHONE_MAX_LENGTH = 13;
|
||||
|
||||
/** `kassaspecification.pdf` — äëÿ `CLIENT` â `MNT_ATTRIBUTES`. */
|
||||
public const KASSA_PHONE_MAX_LENGTH = 19;
|
||||
|
||||
/** @var string */
|
||||
private $name;
|
||||
|
||||
/** @var string */
|
||||
private $email;
|
||||
|
||||
/** @var string */
|
||||
private $phone;
|
||||
|
||||
private function __construct(string $name, string $email, string $phone)
|
||||
{
|
||||
$this->name = $name;
|
||||
$this->email = $email;
|
||||
$this->phone = $phone;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ïîêóïàòåëü èç äàííûõ çàêàçà èëè null, åñëè ÷åêó íåêóäà óéòè. «Email äëÿ ÷åêîâ» ìåð÷àíòà
|
||||
* ïîäñòàâëÿåòñÿ, òîëüêî êîãäà ó ïîêóïàòåëÿ íåò íè email, íè òåëåôîíà; ÔÈÎ îñòà¸òñÿ.
|
||||
*/
|
||||
public static function fromContacts(string $email, string $phone, string $name, string $fallbackEmail): ?self
|
||||
{
|
||||
$phone = PhoneNumber::normalize($phone) ?? '';
|
||||
if (strlen($phone) > self::KASSA_PHONE_MAX_LENGTH) {
|
||||
$phone = '';
|
||||
}
|
||||
|
||||
$email = self::email($email);
|
||||
if (($email === '') && ($phone === '')) {
|
||||
$email = self::email($fallbackEmail);
|
||||
}
|
||||
if (($email === '') && ($phone === '')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new self(ReceiptText::clean($name, self::NAME_MAX_LENGTH), $email, $phone);
|
||||
}
|
||||
|
||||
public function getEmail(): string
|
||||
{
|
||||
return $this->email;
|
||||
}
|
||||
|
||||
public function getPhone(): string
|
||||
{
|
||||
return $this->phone;
|
||||
}
|
||||
|
||||
/**
|
||||
* Îáúåêò `client`; ïóñòûå êëþ÷è íå ïåðåäàþòñÿ.
|
||||
*
|
||||
* @param int $phoneMaxLength {@see self::JSON_PHONE_MAX_LENGTH} èëè {@see self::KASSA_PHONE_MAX_LENGTH}
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function toArray(int $phoneMaxLength): array
|
||||
{
|
||||
$phone = strlen($this->phone) <= $phoneMaxLength ? $this->phone : '';
|
||||
if (($this->email === '') && ($phone === '')) {
|
||||
// Òåëåôîí äëèííåå ñõåìû, à email íåò — áåç êîíòàêòà ÔÈÎ êàññà íå ïðèìåò.
|
||||
return [];
|
||||
}
|
||||
|
||||
return array_filter(
|
||||
['name' => $this->name, 'email' => $this->email, 'phone' => $phone],
|
||||
static function (string $value): bool {
|
||||
return ($value !== '');
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
private static function email(string $email): string
|
||||
{
|
||||
$email = trim($email);
|
||||
if (
|
||||
strlen($email) > self::EMAIL_MAX_LENGTH
|
||||
|| filter_var($email, FILTER_VALIDATE_EMAIL) === false
|
||||
) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return $email;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace MonetaPhpShop\receipt;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use MonetaPhpShop\Money;
|
||||
use MonetaPhpShop\protocol\JsonNumber;
|
||||
|
||||
/**
|
||||
* Ïîçèöèÿ ÷åêà: `inventPositions` (`kassaspecification.pdf`) è ýëåìåíò `items` JSON Check URL
|
||||
* (`cmsspecification.pdf`). Êîëè÷åñòâî — äî òð¸õ çíàêîâ ïîñëå òî÷êè ({@see Quantity});
|
||||
* åäèíèöà èçìåðåíèÿ (`measure`) — èç ïåðå÷íÿ êàññû ({@see Measure}).
|
||||
* Ïðèçíàê ïðåäìåòà ðàñ÷¸òà (`po` / `paymentObject`) — ÿâíî: ó òîâàðîâ èç íàñòðîéêè, ó äîñòàâêè
|
||||
* `service` (áåç íåãî êàññà ñòàâèò `commodity` âñåì, è «Äîñòàâêà» óõîäèëà áû òîâàðîì). Ñïîñîá
|
||||
* ðàñ÷¸òà (`pm` / `paymentMethod`) — ÿâíî: `kassaspecification.pdf` òðåáóåò ïîëå.
|
||||
* Çíà÷åíèÿ — íàñòðîéêè «Ïðèçíàê ïðåäìåòà ðàñ÷¸òà» ({@see PaymentObject}) è
|
||||
* «Ïðèçíàê ñïîñîáà ðàñ÷¸òà» ({@see PaymentMethod}). Îáà ïðèçíàêà — òîëüêî â ÷åêå äëÿ êàññû:
|
||||
* áåç ôèñêàëèçàöèè `INVENTORY` èõ íå íåñ¸ò ({@see self::toInventoryPosition()}).
|
||||
*/
|
||||
final class ReceiptItem
|
||||
{
|
||||
public const NAME_MAX_LENGTH = 128;
|
||||
|
||||
/** Ïîòîëîê êàññû äëÿ öåíû è äëÿ `price * quantity`. */
|
||||
public const PRICE_MAX = '42949672.95';
|
||||
|
||||
/** @var string */
|
||||
private $name;
|
||||
|
||||
/** @var Money */
|
||||
private $price;
|
||||
|
||||
/** @var Quantity */
|
||||
private $quantity;
|
||||
|
||||
/** @var string */
|
||||
private $measure;
|
||||
|
||||
/** @var Vat */
|
||||
private $vat;
|
||||
|
||||
/** @var PaymentObject */
|
||||
private $paymentObject;
|
||||
|
||||
/** @var PaymentMethod */
|
||||
private $paymentMethod;
|
||||
|
||||
/**
|
||||
* @param string $name â UTF-8; ïóñòîå ïîñëå ÷èñòêè êàññà íå ïðèìåò — çàïàñíîå íàçâàíèå
|
||||
* ïîäñòàâëÿåò {@see ReceiptBuilder} (ëèòåðàëû ìîäóëÿ ïîñëå ñáîðêè â cp1251)
|
||||
* @param PaymentObject|null $paymentObject null — òîâàð
|
||||
* @param PaymentMethod|null $paymentMethod null — ïîëíûé ðàñ÷¸ò
|
||||
* @param string $measure êîä åäèíèöû èçìåðåíèÿ êàññû ({@see Measure})
|
||||
*/
|
||||
public function __construct(
|
||||
string $name,
|
||||
Money $price,
|
||||
Quantity $quantity,
|
||||
Vat $vat,
|
||||
?PaymentObject $paymentObject = null,
|
||||
?PaymentMethod $paymentMethod = null,
|
||||
string $measure = Measure::UNIT
|
||||
) {
|
||||
$this->name = ReceiptText::clean($name, self::NAME_MAX_LENGTH);
|
||||
if ($this->name === '') {
|
||||
throw new InvalidArgumentException('Ïóñòîå íàçâàíèå ïîçèöèè ÷åêà.');
|
||||
}
|
||||
$this->price = $price;
|
||||
$this->quantity = $quantity;
|
||||
$this->measure = $measure;
|
||||
$this->vat = $vat;
|
||||
$this->paymentObject = $paymentObject ?? PaymentObject::commodity();
|
||||
$this->paymentMethod = $paymentMethod ?? PaymentMethod::fullPayment();
|
||||
|
||||
$max = Money::fromDecimal(self::PRICE_MAX)->toMinorUnits();
|
||||
if (($price->toMinorUnits() > $max) || ($this->total()->toMinorUnits() > $max)) {
|
||||
throw new InvalidArgumentException('Ñóììà ïîçèöèè ÷åêà âûøå ïîòîëêà êàññû.');
|
||||
}
|
||||
}
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
public function getPrice(): Money
|
||||
{
|
||||
return $this->price;
|
||||
}
|
||||
|
||||
public function getQuantity(): Quantity
|
||||
{
|
||||
return $this->quantity;
|
||||
}
|
||||
|
||||
public function getMeasure(): string
|
||||
{
|
||||
return $this->measure;
|
||||
}
|
||||
|
||||
public function getVat(): Vat
|
||||
{
|
||||
return $this->vat;
|
||||
}
|
||||
|
||||
public function getPaymentObject(): PaymentObject
|
||||
{
|
||||
return $this->paymentObject;
|
||||
}
|
||||
|
||||
public function getPaymentMethod(): PaymentMethod
|
||||
{
|
||||
return $this->paymentMethod;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ñóììà ïîçèöèè òàê, êàê å¸ ïîñ÷èòàåò êàññà: öåíà x êîëè÷åñòâî äî êîïåéêè.
|
||||
*/
|
||||
public function total(): Money
|
||||
{
|
||||
return $this->quantity->totalOf($this->price);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ýëåìåíò `items` JSON-îòâåòà Check URL; öåíà — ìåòêîé {@see JsonNumber}.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toCheckItem(): array
|
||||
{
|
||||
return [
|
||||
'name' => $this->name,
|
||||
'price' => JsonNumber::placeholder($this->price),
|
||||
'quantity' => JsonNumber::quantity($this->quantity),
|
||||
'measure' => $this->measure,
|
||||
'paymentMethod' => $this->paymentMethod->getValue(),
|
||||
'paymentObject' => $this->paymentObject->getValue(),
|
||||
'vat' => $this->vat->getCode(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Îáúåêò `inventPositions` äëÿ àòðèáóòà `INVENTORY`. Áåç ôèñêàëèçàöèè — òîëüêî òî, ÷òî
|
||||
* MONETA.RU ñîõðàíèò â îïåðàöèè: áåç `vatTag`, `pm`, `po`.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toInventoryPosition(bool $fiscalization = true): array
|
||||
{
|
||||
$position = [
|
||||
'name' => $this->name,
|
||||
'price' => JsonNumber::placeholder($this->price),
|
||||
'quantity' => JsonNumber::quantity($this->quantity),
|
||||
'measure' => $this->measure,
|
||||
];
|
||||
if (!$fiscalization) {
|
||||
return $position;
|
||||
}
|
||||
|
||||
return $position + [
|
||||
'vatTag' => $this->vat->getTag(),
|
||||
'pm' => $this->paymentMethod->getValue(),
|
||||
'po' => $this->paymentObject->getValue(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace MonetaPhpShop\receipt;
|
||||
|
||||
/**
|
||||
* Òåêñòîâûå ïîëÿ ÷åêà (íàçâàíèÿ ïîçèöèé, ÔÈÎ), ñòðîêè â UTF-8: «çíà÷åíèÿ íå äîëæíû ñîäåðæàòü
|
||||
* êàâû÷åê, çíàêîâ &, $, #, îáðàòíûõ è ïðÿìûõ ñëýøåé» (`cmsspecification.pdf`); êîíòðàêò ìîäóëåé
|
||||
* äîáàâëÿåò `%` è `;`. Îñòàâëÿåì áåëûé ñïèñîê: áóêâû, öèôðû, ïðîáåë è `.,()_¹+-`.
|
||||
*
|
||||
* Òîëüêî ASCII â øàáëîíàõ (`¹` — `\x{2116}`): ïîñëå ñáîðêè ôàéë â cp1251, à øàáëîí ñ `/u`
|
||||
* îáÿçàí áûòü UTF-8. Áåç `mbstring` — ÿäðî PHPShop åãî íå òðåáóåò.
|
||||
*/
|
||||
final class ReceiptText
|
||||
{
|
||||
private const ELLIPSIS = '...';
|
||||
|
||||
private function __construct() {}
|
||||
|
||||
public static function clean(string $value, int $maxLength): string
|
||||
{
|
||||
$value = self::sanitize($value);
|
||||
if ((int) preg_match_all('/./su', $value) <= $maxLength) {
|
||||
return $value;
|
||||
}
|
||||
preg_match('/\A.{0,' . ($maxLength - strlen(self::ELLIPSIS)) . '}/su', $value, $matches);
|
||||
|
||||
return rtrim($matches[0] ?? '') . self::ELLIPSIS;
|
||||
}
|
||||
|
||||
private static function sanitize(string $value): string
|
||||
{
|
||||
$decoded = html_entity_decode(strip_tags($value), ENT_QUOTES | ENT_HTML5, 'UTF-8');
|
||||
$allowed = preg_replace('/[^\p{L}\p{N}\s.,()_\x{2116}+\-]/u', ' ', $decoded) ?? '';
|
||||
|
||||
return trim(preg_replace('/\s+/u', ' ', $allowed) ?? '');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace MonetaPhpShop\receipt;
|
||||
|
||||
/**
|
||||
* Система налогообложения — атрибут `SNO` ответа Pay URL (`kassaspecification.pdf`, необязательный).
|
||||
* Передаётся только при фискализации и только если мерчант её выбрал; по умолчанию
|
||||
* не передаётся — касса берёт систему налогообложения из своих настроек. В JSON Check URL поля нет
|
||||
* (схема `cmsspecification.pdf`). Значения хранятся в настройках модуля — не переименовывать.
|
||||
*/
|
||||
final class TaxSystem
|
||||
{
|
||||
/** Не передавать: система налогообложения из настроек кассы. */
|
||||
public const KASSA = '';
|
||||
|
||||
/** Значение -> подпись в настройках, коды — по `kassaspecification.pdf`. */
|
||||
public const LABELS = [
|
||||
self::KASSA => 'Не передавать (из настроек кассы)',
|
||||
'1' => 'Общая (ОСНО)',
|
||||
'2' => 'Упрощённая (УСН) доход',
|
||||
'3' => 'Упрощённая (УСН) доход минус расход',
|
||||
'4' => 'Единый налог на вменённый доход (ЕНВД)',
|
||||
'5' => 'Единый сельскохозяйственный налог (ЕСХН)',
|
||||
'6' => 'Патентная (ПСН)',
|
||||
];
|
||||
|
||||
/** @var string */
|
||||
private $value;
|
||||
|
||||
private function __construct(string $value)
|
||||
{
|
||||
$this->value = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Значение из настроек; неизвестное или пустое — не передавать.
|
||||
*/
|
||||
public static function fromSetting(?string $value): self
|
||||
{
|
||||
$value = (string) $value;
|
||||
|
||||
return new self(isset(self::LABELS[$value]) ? $value : self::KASSA);
|
||||
}
|
||||
|
||||
public static function kassa(): self
|
||||
{
|
||||
return new self(self::KASSA);
|
||||
}
|
||||
|
||||
public function getValue(): string
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
public function isPassed(): bool
|
||||
{
|
||||
return ($this->value !== self::KASSA);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace MonetaPhpShop\receipt;
|
||||
|
||||
/**
|
||||
* Ñòàâêà ÍÄÑ ïîçèöèè ÷åêà: êîä `vat` äëÿ JSON Check URL (`cmsspecification.pdf`)
|
||||
* è òåã `vatTag` äëÿ `INVENTORY` (`kassaspecification.pdf`).
|
||||
*
|
||||
* Èç íàñòðîåê PHPShop ìîäóëü áåð¸ò òîëüêî ïðÿìûå ñòàâêè ïî ïðîöåíòó. Ðàñ÷¸òíûå (`vat120`, `1106`…)
|
||||
* ïðèâÿçàíû ê àâàíñàì è ïðåäîïëàòå (`pm` `advance`/`prepayment`) — èõ ìîäóëü ñàì íå âûáèðàåò,
|
||||
* òîëüêî ìåð÷àíò íàñòðîéêîé ({@see VatSetting}).
|
||||
* Value-îáúåêò âìåñòî enum — PHP 7.4.
|
||||
*/
|
||||
final class Vat
|
||||
{
|
||||
public const NONE = 'none';
|
||||
|
||||
/** Ïðîöåíò ñòàâêè -> êîä `vat`. */
|
||||
private const BY_PERCENT = [
|
||||
0 => 'vat0',
|
||||
5 => 'vat5',
|
||||
7 => 'vat7',
|
||||
10 => 'vat10',
|
||||
20 => 'vat20',
|
||||
22 => 'vat22',
|
||||
];
|
||||
|
||||
/**
|
||||
* Êîä `vat` (`cmsspecification.pdf`) -> `vatTag` (`kassaspecification.pdf`).
|
||||
*/
|
||||
private const TAGS = [
|
||||
self::NONE => '1105',
|
||||
'vat0' => '1104',
|
||||
'vat5' => '1108',
|
||||
'vat7' => '1109',
|
||||
'vat10' => '1103',
|
||||
'vat20' => '1102',
|
||||
'vat22' => '1113',
|
||||
'vat105' => '1110',
|
||||
'vat107' => '1111',
|
||||
'vat110' => '1107',
|
||||
'vat120' => '1106',
|
||||
'vat122' => '1114',
|
||||
];
|
||||
|
||||
/** @var string */
|
||||
private $code;
|
||||
|
||||
private function __construct(string $code)
|
||||
{
|
||||
$this->code = $code;
|
||||
}
|
||||
|
||||
public static function none(): self
|
||||
{
|
||||
return new self(self::NONE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ñòàâêà ïî êîäó `vat`; íåèçâåñòíûé êîä — null.
|
||||
*/
|
||||
public static function fromCode(string $code): ?self
|
||||
{
|
||||
return isset(self::TAGS[$code]) ? new self($code) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ñòàâêà èç íàñòðîåê PHPShop — ñâîáîäíîå ÷èñëî ïðîöåíòîâ (`nds`, `ofd_nds` äîñòàâêè).
|
||||
* Ïóñòî èëè ñòàâêè íåò â ïåðå÷íå êàññû — «áåç ÍÄÑ»: íåèçâåñòíóþ ñòàâêó íå óãàäûâàåì.
|
||||
*/
|
||||
public static function fromPercent(string $percent): self
|
||||
{
|
||||
return self::tryFromPercent($percent) ?? self::none();
|
||||
}
|
||||
|
||||
/**
|
||||
* Êàê {@see self::fromPercent()}, íî ïóñòîå èëè íåèçâåñòíîå çíà÷åíèå — null: âûçûâàþùèé
|
||||
* ïðåäóïðåæäàåò ìåð÷àíòà, ÷òî ïîçèöèÿ óéä¸ò â ÷åê «áåç ÍÄÑ».
|
||||
*/
|
||||
public static function tryFromPercent(string $percent): ?self
|
||||
{
|
||||
$percent = trim($percent);
|
||||
if (preg_match('/\A[0-9]{1,3}(?:[.,]0+)?\z/', $percent) !== 1) {
|
||||
return null;
|
||||
}
|
||||
$value = (int) $percent;
|
||||
|
||||
return isset(self::BY_PERCENT[$value]) ? new self(self::BY_PERCENT[$value]) : null;
|
||||
}
|
||||
|
||||
public function getCode(): string
|
||||
{
|
||||
return $this->code;
|
||||
}
|
||||
|
||||
public function getTag(): string
|
||||
{
|
||||
return self::TAGS[$this->code];
|
||||
}
|
||||
|
||||
public function equals(self $other): bool
|
||||
{
|
||||
return ($this->code === $other->code);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace MonetaPhpShop\receipt;
|
||||
|
||||
/**
|
||||
* Ñòàâêè ÍÄÑ èç íàñòðîåê PHPShop, êîòîðûõ íåò â ïåðå÷íå êàññû: ïîçèöèè ñ íèìè óõîäÿò â ÷åê
|
||||
* «áåç ÍÄÑ» ({@see Vat::fromPercent()}) — ìîäóëü ñòàâêó íå óãàäûâàåò, íî ìåð÷àíò äîëæåí îá ýòîì
|
||||
* çíàòü. Èñòî÷íèê ïðåäóïðåæäåíèÿ â íàñòðîéêàõ ìîäóëÿ.
|
||||
*
|
||||
* Ïðîâåðÿåòñÿ òîëüêî òî, ÷òî ìîäóëü äåéñòâèòåëüíî áåð¸ò èç PHPShop: ïðè ôèñêàëèçàöèè,
|
||||
* âêëþ÷¸ííîì ÍÄÑ ìàãàçèíà è íàñòðîéêå «èç PHPShop» ó òîâàðîâ èëè äîñòàâêè.
|
||||
*/
|
||||
final class VatRateWarnings
|
||||
{
|
||||
private function __construct() {}
|
||||
|
||||
/**
|
||||
* @param string $systemPercent ñèñòåìíàÿ ñòàâêà PHPShop (`nds`)
|
||||
* @param array<int, array{0: string, 1: string}> $deliveries ñïîñîáû äîñòàâêè: [íàçâàíèå, «Çíà÷åíèå ÍÄÑ»
|
||||
* (`ofd_nds`, ïóñòî — ñèñòåìíàÿ ñòàâêà)]
|
||||
*
|
||||
* @return array<int, string> íåðàñïîçíàííûå ñòàâêè: `ñèñòåìíàÿ ñòàâêà PHPShop (òîâàðû): 23`
|
||||
*/
|
||||
public static function find(
|
||||
bool $fiscalization,
|
||||
bool $vatEnabled,
|
||||
VatSetting $goodsVat,
|
||||
VatSetting $deliveryVat,
|
||||
string $systemPercent,
|
||||
array $deliveries
|
||||
): array {
|
||||
if (!$fiscalization || !$vatEnabled) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$warnings = [];
|
||||
if ($goodsVat->getFixed() === null && Vat::tryFromPercent($systemPercent) === null) {
|
||||
$warnings[] = 'ñèñòåìíàÿ ñòàâêà PHPShop (òîâàðû): ' . self::shown($systemPercent);
|
||||
}
|
||||
if ($deliveryVat->getFixed() === null) {
|
||||
foreach ($deliveries as [$name, $percent]) {
|
||||
$percent = (trim($percent) !== '') ? $percent : $systemPercent;
|
||||
if (Vat::tryFromPercent($percent) === null) {
|
||||
$warnings[] = 'äîñòàâêà «' . $name . '»: ' . self::shown($percent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $warnings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Çíà÷åíèå äëÿ ñîîáùåíèÿ: ïóñòîå — ñëîâîì, èíà÷å êàê ââ¸ë ìåð÷àíò.
|
||||
*/
|
||||
public static function shown(string $percent): string
|
||||
{
|
||||
$percent = trim($percent);
|
||||
|
||||
return ($percent === '') ? 'íå çàäàíà' : $percent . '%';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace MonetaPhpShop\receipt;
|
||||
|
||||
/**
|
||||
* Íàñòðîéêè «ÍÄÑ íà òîâàðû» è «ÍÄÑ íà äîñòàâêó»: ñòàâêà èç PHPShop
|
||||
* (ïî óìîë÷àíèþ) èëè îäíà ôèêñèðîâàííàÿ ñòàâêà íà âñå ïîçèöèè, âêëþ÷àÿ ðàñ÷¸òíûå —
|
||||
* èõ ïðè àâàíñå è ïðåäîïëàòå âûáèðàåò ìåð÷àíò, ìîäóëü ñàì íå ïîäñòàâëÿåò.
|
||||
* Çíà÷åíèÿ õðàíÿòñÿ â íàñòðîéêàõ ìîäóëÿ — íå ïåðåèìåíîâûâàòü.
|
||||
*/
|
||||
final class VatSetting
|
||||
{
|
||||
/** Ñòàâêà èç PHPShop: ñèñòåìíàÿ ó òîâàðîâ, ñïîñîáà äîñòàâêè ó äîñòàâêè. */
|
||||
public const PLATFORM = 'platform';
|
||||
|
||||
/** Ôèêñèðîâàííûå ñòàâêè: êîä `vat` -> ïîäïèñü â íàñòðîéêàõ. */
|
||||
public const FIXED_LABELS = [
|
||||
Vat::NONE => 'Áåç ÍÄÑ',
|
||||
'vat0' => 'ÍÄÑ 0%',
|
||||
'vat5' => 'ÍÄÑ 5%',
|
||||
'vat7' => 'ÍÄÑ 7%',
|
||||
'vat10' => 'ÍÄÑ 10%',
|
||||
'vat20' => 'ÍÄÑ 20%',
|
||||
'vat22' => 'ÍÄÑ 22%',
|
||||
'vat105' => 'ÍÄÑ 5% (ðàñ÷¸òíàÿ ñòàâêà 5/105)',
|
||||
'vat107' => 'ÍÄÑ 7% (ðàñ÷¸òíàÿ ñòàâêà 7/107)',
|
||||
'vat110' => 'ÍÄÑ 10% (ðàñ÷¸òíàÿ ñòàâêà 10/110)',
|
||||
'vat120' => 'ÍÄÑ 20% (ðàñ÷¸òíàÿ ñòàâêà 20/120)',
|
||||
'vat122' => 'ÍÄÑ 22% (ðàñ÷¸òíàÿ ñòàâêà 22/122)',
|
||||
];
|
||||
|
||||
/** @var Vat|null */
|
||||
private $fixed;
|
||||
|
||||
private function __construct(?Vat $fixed)
|
||||
{
|
||||
$this->fixed = $fixed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Çíà÷åíèå èç íàñòðîåê; ïóñòîå, {@see self::PLATFORM} èëè íåèçâåñòíîå — ñòàâêà èç PHPShop.
|
||||
*/
|
||||
public static function fromSetting(?string $value): self
|
||||
{
|
||||
$value = (string) $value;
|
||||
|
||||
return new self(isset(self::FIXED_LABELS[$value]) ? Vat::fromCode($value) : null);
|
||||
}
|
||||
|
||||
public static function platform(): self
|
||||
{
|
||||
return new self(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ôèêñèðîâàííàÿ ñòàâêà èëè null — áðàòü èç PHPShop.
|
||||
*/
|
||||
public function getFixed(): ?Vat
|
||||
{
|
||||
return $this->fixed;
|
||||
}
|
||||
|
||||
public function getValue(): string
|
||||
{
|
||||
return ($this->fixed !== null) ? $this->fixed->getCode() : self::PLATFORM;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ïîäïèñè äëÿ âûïàäàþùåãî ñïèñêà: ïåðâûì — ñòàâêà èç PHPShop ñ ïîäïèñüþ `$platformLabel`.
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public static function labels(string $platformLabel): array
|
||||
{
|
||||
return [self::PLATFORM => $platformLabel] + self::FIXED_LABELS;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user