feat: add Order, Pipeline, Auth domain entities (Customer with matching-key, Order, Invoice, AIPipelineJob, User, ApiKey)
This commit is contained in:
parent
11c894b8a4
commit
e8fb01f707
7 changed files with 559 additions and 0 deletions
81
src/Domain/Auth/ApiKey.php
Normal file
81
src/Domain/Auth/ApiKey.php
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Domain\Auth;
|
||||
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Uid\Uuid;
|
||||
|
||||
#[ORM\Entity]
|
||||
#[ORM\Table(name: 'api_keys', schema: 'app')]
|
||||
class ApiKey
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\Column(type: 'uuid')]
|
||||
private Uuid $id;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: User::class)]
|
||||
#[ORM\JoinColumn(nullable: false)]
|
||||
private User $user;
|
||||
|
||||
#[ORM\Column(type: 'string', length: 255)]
|
||||
private string $label;
|
||||
|
||||
#[ORM\Column(type: 'string', length: 255, unique: true)]
|
||||
private string $keyHash;
|
||||
|
||||
/** @var array<string, bool> */
|
||||
#[ORM\Column(type: 'json')]
|
||||
private array $permissions = [];
|
||||
|
||||
#[ORM\Column(type: 'boolean')]
|
||||
private bool $isActive = true;
|
||||
|
||||
#[ORM\Column(type: 'datetime_immutable', nullable: true)]
|
||||
private ?\DateTimeImmutable $lastUsedAt = null;
|
||||
|
||||
#[ORM\Column(type: 'datetime_immutable', nullable: true)]
|
||||
private ?\DateTimeImmutable $expiresAt = null;
|
||||
|
||||
public function __construct(User $user, string $label, string $keyHash)
|
||||
{
|
||||
$this->id = Uuid::v7();
|
||||
$this->user = $user;
|
||||
$this->label = $label;
|
||||
$this->keyHash = $keyHash;
|
||||
}
|
||||
|
||||
public function getId(): Uuid { return $this->id; }
|
||||
public function getUser(): User { return $this->user; }
|
||||
public function getLabel(): string { return $this->label; }
|
||||
public function getKeyHash(): string { return $this->keyHash; }
|
||||
public function isActive(): bool { return $this->isActive; }
|
||||
public function setIsActive(bool $active): void { $this->isActive = $active; }
|
||||
public function getLastUsedAt(): ?\DateTimeImmutable { return $this->lastUsedAt; }
|
||||
public function getExpiresAt(): ?\DateTimeImmutable { return $this->expiresAt; }
|
||||
public function setExpiresAt(?\DateTimeImmutable $expiresAt): void { $this->expiresAt = $expiresAt; }
|
||||
|
||||
/** @return array<string, bool> */
|
||||
public function getPermissions(): array { return $this->permissions; }
|
||||
|
||||
public function hasPermission(string $permission): bool
|
||||
{
|
||||
return $this->permissions[$permission] ?? false;
|
||||
}
|
||||
|
||||
public function grantPermission(string $permission): void
|
||||
{
|
||||
$this->permissions[$permission] = true;
|
||||
}
|
||||
|
||||
public function markUsed(): void
|
||||
{
|
||||
$this->lastUsedAt = new \DateTimeImmutable();
|
||||
}
|
||||
|
||||
public function isExpired(): bool
|
||||
{
|
||||
return null !== $this->expiresAt && $this->expiresAt < new \DateTimeImmutable();
|
||||
}
|
||||
}
|
||||
75
src/Domain/Auth/User.php
Normal file
75
src/Domain/Auth/User.php
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Domain\Auth;
|
||||
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
|
||||
use Symfony\Component\Security\Core\User\UserInterface;
|
||||
use Symfony\Component\Uid\Uuid;
|
||||
|
||||
#[ORM\Entity]
|
||||
#[ORM\Table(name: 'users', schema: 'app')]
|
||||
class User implements UserInterface, PasswordAuthenticatedUserInterface
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\Column(type: 'uuid')]
|
||||
private Uuid $id;
|
||||
|
||||
#[ORM\Column(type: 'string', length: 255, unique: true)]
|
||||
private string $email;
|
||||
|
||||
#[ORM\Column(type: 'string')]
|
||||
private string $passwordHash;
|
||||
|
||||
#[ORM\Column(type: 'string', nullable: true)]
|
||||
private ?string $totpSecret = null;
|
||||
|
||||
/** @var array<string, bool> */
|
||||
#[ORM\Column(type: 'json')]
|
||||
private array $permissions = [];
|
||||
|
||||
#[ORM\Column(type: 'boolean')]
|
||||
private bool $isActive = true;
|
||||
|
||||
public function __construct(string $email, string $passwordHash)
|
||||
{
|
||||
$this->id = Uuid::v7();
|
||||
$this->email = $email;
|
||||
$this->passwordHash = $passwordHash;
|
||||
}
|
||||
|
||||
public function getId(): Uuid { return $this->id; }
|
||||
public function getEmail(): string { return $this->email; }
|
||||
public function getPassword(): string { return $this->passwordHash; }
|
||||
public function getUserIdentifier(): string { return $this->email; }
|
||||
|
||||
/** @return list<string> */
|
||||
public function getRoles(): array { return ['ROLE_USER']; }
|
||||
|
||||
public function eraseCredentials(): void {}
|
||||
|
||||
public function getTotpSecret(): ?string { return $this->totpSecret; }
|
||||
public function setTotpSecret(?string $secret): void { $this->totpSecret = $secret; }
|
||||
public function isActive(): bool { return $this->isActive; }
|
||||
public function setIsActive(bool $active): void { $this->isActive = $active; }
|
||||
|
||||
/** @return array<string, bool> */
|
||||
public function getPermissions(): array { return $this->permissions; }
|
||||
|
||||
public function hasPermission(string $permission): bool
|
||||
{
|
||||
return $this->permissions[$permission] ?? false;
|
||||
}
|
||||
|
||||
public function grantPermission(string $permission): void
|
||||
{
|
||||
$this->permissions[$permission] = true;
|
||||
}
|
||||
|
||||
public function revokePermission(string $permission): void
|
||||
{
|
||||
unset($this->permissions[$permission]);
|
||||
}
|
||||
}
|
||||
78
src/Domain/Order/Customer.php
Normal file
78
src/Domain/Order/Customer.php
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Domain\Order;
|
||||
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Uid\Uuid;
|
||||
|
||||
#[ORM\Entity]
|
||||
#[ORM\Table(name: 'customers', schema: 'app')]
|
||||
class Customer
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\Column(type: 'uuid')]
|
||||
private Uuid $id;
|
||||
|
||||
#[ORM\Column(type: 'string', length: 255)]
|
||||
private string $name;
|
||||
|
||||
#[ORM\Column(type: 'string', length: 255)]
|
||||
private string $email;
|
||||
|
||||
/** @var array<string, string> */
|
||||
#[ORM\Column(type: 'json')]
|
||||
private array $address;
|
||||
|
||||
#[ORM\Column(type: 'string', length: 255, nullable: true)]
|
||||
private ?string $frappeCustomerId = null;
|
||||
|
||||
/** @var array<string, string> */
|
||||
#[ORM\Column(type: 'json')]
|
||||
private array $platformIds = [];
|
||||
|
||||
/**
|
||||
* @param array<string, string> $address
|
||||
*/
|
||||
public function __construct(string $name, string $email, array $address)
|
||||
{
|
||||
$this->id = Uuid::v7();
|
||||
$this->name = $name;
|
||||
$this->email = $email;
|
||||
$this->address = $address;
|
||||
}
|
||||
|
||||
public function getId(): Uuid { return $this->id; }
|
||||
public function getName(): string { return $this->name; }
|
||||
public function getEmail(): string { return $this->email; }
|
||||
|
||||
/** @return array<string, string> */
|
||||
public function getAddress(): array { return $this->address; }
|
||||
|
||||
public function getFrappeCustomerId(): ?string { return $this->frappeCustomerId; }
|
||||
public function setFrappeCustomerId(?string $id): void { $this->frappeCustomerId = $id; }
|
||||
|
||||
/** @return array<string, string> */
|
||||
public function getPlatformIds(): array { return $this->platformIds; }
|
||||
|
||||
public function getPlatformId(string $platform): ?string
|
||||
{
|
||||
return $this->platformIds[$platform] ?? null;
|
||||
}
|
||||
|
||||
public function addPlatformId(string $platform, string $id): void
|
||||
{
|
||||
$this->platformIds[$platform] = $id;
|
||||
}
|
||||
|
||||
public function getMatchingKey(): string
|
||||
{
|
||||
return \mb_strtolower(\implode('|', [
|
||||
$this->name,
|
||||
$this->address['street'] ?? '',
|
||||
$this->address['city'] ?? '',
|
||||
$this->address['zip'] ?? '',
|
||||
]));
|
||||
}
|
||||
}
|
||||
70
src/Domain/Order/Invoice.php
Normal file
70
src/Domain/Order/Invoice.php
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Domain\Order;
|
||||
|
||||
use App\Domain\Storage\StoragePath;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Uid\Uuid;
|
||||
|
||||
#[ORM\Entity]
|
||||
#[ORM\Table(name: 'invoices', schema: 'app')]
|
||||
class Invoice
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\Column(type: 'uuid')]
|
||||
private Uuid $id;
|
||||
|
||||
#[ORM\OneToOne(targetEntity: Order::class, inversedBy: 'invoice')]
|
||||
#[ORM\JoinColumn(nullable: false)]
|
||||
private Order $order;
|
||||
|
||||
#[ORM\Column(type: 'string', length: 255)]
|
||||
private string $frappeInvoiceId;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: StoragePath::class)]
|
||||
#[ORM\JoinColumn(nullable: false)]
|
||||
private StoragePath $storagePath;
|
||||
|
||||
#[ORM\Column(type: 'string', length: 500)]
|
||||
private string $filename;
|
||||
|
||||
#[ORM\Column(type: 'datetime_immutable')]
|
||||
private \DateTimeImmutable $createdAt;
|
||||
|
||||
#[ORM\Column(type: 'datetime_immutable', nullable: true)]
|
||||
private ?\DateTimeImmutable $emailedAt = null;
|
||||
|
||||
public function __construct(
|
||||
Order $order,
|
||||
string $frappeInvoiceId,
|
||||
StoragePath $storagePath,
|
||||
string $filename,
|
||||
) {
|
||||
$this->id = Uuid::v7();
|
||||
$this->order = $order;
|
||||
$this->frappeInvoiceId = $frappeInvoiceId;
|
||||
$this->storagePath = $storagePath;
|
||||
$this->filename = $filename;
|
||||
$this->createdAt = new \DateTimeImmutable();
|
||||
}
|
||||
|
||||
public function getId(): Uuid { return $this->id; }
|
||||
public function getOrder(): Order { return $this->order; }
|
||||
public function getFrappeInvoiceId(): string { return $this->frappeInvoiceId; }
|
||||
public function getStoragePath(): StoragePath { return $this->storagePath; }
|
||||
public function getFilename(): string { return $this->filename; }
|
||||
public function getCreatedAt(): \DateTimeImmutable { return $this->createdAt; }
|
||||
public function getEmailedAt(): ?\DateTimeImmutable { return $this->emailedAt; }
|
||||
|
||||
public function markAsEmailed(): void
|
||||
{
|
||||
$this->emailedAt = new \DateTimeImmutable();
|
||||
}
|
||||
|
||||
public function getFullPath(): string
|
||||
{
|
||||
return $this->storagePath->resolveFilePath($this->filename);
|
||||
}
|
||||
}
|
||||
105
src/Domain/Order/Order.php
Normal file
105
src/Domain/Order/Order.php
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Domain\Order;
|
||||
|
||||
use App\Domain\Article\Article;
|
||||
use App\Domain\Channel\Platform;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Uid\Uuid;
|
||||
|
||||
#[ORM\Entity]
|
||||
#[ORM\Table(name: 'orders', schema: 'app')]
|
||||
class Order
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\Column(type: 'uuid')]
|
||||
private Uuid $id;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: Article::class)]
|
||||
#[ORM\JoinColumn(nullable: false)]
|
||||
private Article $article;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: Customer::class)]
|
||||
#[ORM\JoinColumn(nullable: false)]
|
||||
private Customer $customer;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: Platform::class)]
|
||||
#[ORM\JoinColumn(nullable: false)]
|
||||
private Platform $platform;
|
||||
|
||||
#[ORM\Column(type: 'string', length: 255)]
|
||||
private string $platformOrderId;
|
||||
|
||||
#[ORM\Column(type: 'string', enumType: OrderStatus::class)]
|
||||
private OrderStatus $status;
|
||||
|
||||
#[ORM\Column(type: 'decimal', precision: 10, scale: 2)]
|
||||
private string $salePrice;
|
||||
|
||||
#[ORM\Column(type: 'datetime_immutable')]
|
||||
private \DateTimeImmutable $saleDate;
|
||||
|
||||
#[ORM\Column(type: 'string', length: 255, nullable: true)]
|
||||
private ?string $trackingNumber = null;
|
||||
|
||||
#[ORM\Column(type: 'string', length: 100, nullable: true)]
|
||||
private ?string $carrier = null;
|
||||
|
||||
#[ORM\Column(type: 'datetime_immutable', nullable: true)]
|
||||
private ?\DateTimeImmutable $shippedAt = null;
|
||||
|
||||
#[ORM\Column(type: 'datetime_immutable', nullable: true)]
|
||||
private ?\DateTimeImmutable $trackingPushedToEbayAt = null;
|
||||
|
||||
#[ORM\OneToOne(mappedBy: 'order', targetEntity: Invoice::class, cascade: ['persist'])]
|
||||
private ?Invoice $invoice = null;
|
||||
|
||||
public function __construct(
|
||||
Article $article,
|
||||
Customer $customer,
|
||||
Platform $platform,
|
||||
string $platformOrderId,
|
||||
string $salePrice,
|
||||
\DateTimeImmutable $saleDate,
|
||||
) {
|
||||
$this->id = Uuid::v7();
|
||||
$this->article = $article;
|
||||
$this->customer = $customer;
|
||||
$this->platform = $platform;
|
||||
$this->platformOrderId = $platformOrderId;
|
||||
$this->status = OrderStatus::Pending;
|
||||
$this->salePrice = $salePrice;
|
||||
$this->saleDate = $saleDate;
|
||||
}
|
||||
|
||||
public function setTracking(string $trackingNumber, string $carrier): void
|
||||
{
|
||||
$this->trackingNumber = $trackingNumber;
|
||||
$this->carrier = $carrier;
|
||||
$this->shippedAt = new \DateTimeImmutable();
|
||||
$this->status = OrderStatus::Shipped;
|
||||
}
|
||||
|
||||
public function markTrackingPushedToEbay(): void
|
||||
{
|
||||
$this->trackingPushedToEbayAt = new \DateTimeImmutable();
|
||||
}
|
||||
|
||||
public function getId(): Uuid { return $this->id; }
|
||||
public function getArticle(): Article { return $this->article; }
|
||||
public function getCustomer(): Customer { return $this->customer; }
|
||||
public function getPlatform(): Platform { return $this->platform; }
|
||||
public function getPlatformOrderId(): string { return $this->platformOrderId; }
|
||||
public function getStatus(): OrderStatus { return $this->status; }
|
||||
public function setStatus(OrderStatus $status): void { $this->status = $status; }
|
||||
public function getSalePrice(): string { return $this->salePrice; }
|
||||
public function getSaleDate(): \DateTimeImmutable { return $this->saleDate; }
|
||||
public function getTrackingNumber(): ?string { return $this->trackingNumber; }
|
||||
public function getCarrier(): ?string { return $this->carrier; }
|
||||
public function getShippedAt(): ?\DateTimeImmutable { return $this->shippedAt; }
|
||||
public function getTrackingPushedToEbayAt(): ?\DateTimeImmutable { return $this->trackingPushedToEbayAt; }
|
||||
public function getInvoice(): ?Invoice { return $this->invoice; }
|
||||
public function setInvoice(Invoice $invoice): void { $this->invoice = $invoice; }
|
||||
}
|
||||
111
src/Domain/Pipeline/AIPipelineJob.php
Normal file
111
src/Domain/Pipeline/AIPipelineJob.php
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Domain\Pipeline;
|
||||
|
||||
use App\Domain\Article\Article;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Uid\Uuid;
|
||||
|
||||
#[ORM\Entity]
|
||||
#[ORM\Table(name: 'ai_pipeline_jobs', schema: 'app')]
|
||||
class AIPipelineJob
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\Column(type: 'uuid')]
|
||||
private Uuid $id;
|
||||
|
||||
#[ORM\Column(type: 'string', enumType: AIPipelineJobType::class)]
|
||||
private AIPipelineJobType $type;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: Article::class)]
|
||||
#[ORM\JoinColumn(nullable: true)]
|
||||
private ?Article $article = null;
|
||||
|
||||
#[ORM\Column(type: 'string', enumType: AIPipelineJobStatus::class)]
|
||||
private AIPipelineJobStatus $status;
|
||||
|
||||
#[ORM\Column(type: 'integer')]
|
||||
private int $attemptCount = 0;
|
||||
|
||||
/** @var array<string, mixed> */
|
||||
#[ORM\Column(type: 'json')]
|
||||
private array $inputData;
|
||||
|
||||
/** @var array<string, mixed> */
|
||||
#[ORM\Column(type: 'json')]
|
||||
private array $outputData = [];
|
||||
|
||||
/** @var list<string> */
|
||||
#[ORM\Column(type: 'simple_array', nullable: true)]
|
||||
private array $missingFields = [];
|
||||
|
||||
#[ORM\Column(type: 'text', nullable: true)]
|
||||
private ?string $errorMessage = null;
|
||||
|
||||
#[ORM\Column(type: 'datetime_immutable')]
|
||||
private \DateTimeImmutable $createdAt;
|
||||
|
||||
#[ORM\Column(type: 'datetime_immutable', nullable: true)]
|
||||
private ?\DateTimeImmutable $completedAt = null;
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $inputData
|
||||
*/
|
||||
public function __construct(AIPipelineJobType $type, array $inputData)
|
||||
{
|
||||
$this->id = Uuid::v7();
|
||||
$this->type = $type;
|
||||
$this->inputData = $inputData;
|
||||
$this->status = AIPipelineJobStatus::Queued;
|
||||
$this->createdAt = new \DateTimeImmutable();
|
||||
}
|
||||
|
||||
public function incrementAttempt(): void { ++$this->attemptCount; }
|
||||
|
||||
public function markCompleted(): void
|
||||
{
|
||||
$this->status = AIPipelineJobStatus::Completed;
|
||||
$this->completedAt = new \DateTimeImmutable();
|
||||
}
|
||||
|
||||
public function markFailed(string $errorMessage): void
|
||||
{
|
||||
$this->status = AIPipelineJobStatus::Failed;
|
||||
$this->errorMessage = $errorMessage;
|
||||
}
|
||||
|
||||
public function markNeedsReview(string $reason): void
|
||||
{
|
||||
$this->status = AIPipelineJobStatus::NeedsReview;
|
||||
$this->errorMessage = $reason;
|
||||
}
|
||||
|
||||
public function getId(): Uuid { return $this->id; }
|
||||
public function getType(): AIPipelineJobType { return $this->type; }
|
||||
public function getArticle(): ?Article { return $this->article; }
|
||||
public function setArticle(Article $article): void { $this->article = $article; }
|
||||
public function getStatus(): AIPipelineJobStatus { return $this->status; }
|
||||
public function setStatus(AIPipelineJobStatus $status): void { $this->status = $status; }
|
||||
public function getAttemptCount(): int { return $this->attemptCount; }
|
||||
|
||||
/** @return array<string, mixed> */
|
||||
public function getInputData(): array { return $this->inputData; }
|
||||
|
||||
/** @return array<string, mixed> */
|
||||
public function getOutputData(): array { return $this->outputData; }
|
||||
|
||||
/** @param array<string, mixed> $outputData */
|
||||
public function setOutputData(array $outputData): void { $this->outputData = $outputData; }
|
||||
|
||||
/** @return list<string> */
|
||||
public function getMissingFields(): array { return $this->missingFields; }
|
||||
|
||||
/** @param list<string> $fields */
|
||||
public function setMissingFields(array $fields): void { $this->missingFields = $fields; }
|
||||
|
||||
public function getErrorMessage(): ?string { return $this->errorMessage; }
|
||||
public function getCreatedAt(): \DateTimeImmutable { return $this->createdAt; }
|
||||
public function getCompletedAt(): ?\DateTimeImmutable { return $this->completedAt; }
|
||||
}
|
||||
39
tests/Unit/Domain/Order/CustomerTest.php
Normal file
39
tests/Unit/Domain/Order/CustomerTest.php
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Unit\Domain\Order;
|
||||
|
||||
use App\Domain\Order\Customer;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
final class CustomerTest extends TestCase
|
||||
{
|
||||
public function test_new_customer_has_empty_platform_ids(): void
|
||||
{
|
||||
$customer = new Customer('Max Mustermann', 'max@example.com', []);
|
||||
|
||||
$this->assertSame([], $customer->getPlatformIds());
|
||||
$this->assertNull($customer->getPlatformId('ebay'));
|
||||
}
|
||||
|
||||
public function test_add_platform_id(): void
|
||||
{
|
||||
$customer = new Customer('Max Mustermann', 'max@example.com', []);
|
||||
$customer->addPlatformId('ebay', 'ebay-user-123');
|
||||
|
||||
$this->assertSame('ebay-user-123', $customer->getPlatformId('ebay'));
|
||||
}
|
||||
|
||||
public function test_matching_key_is_lowercase_normalized(): void
|
||||
{
|
||||
$customer = new Customer('Max Mustermann', 'max@example.com', [
|
||||
'street' => 'Musterstraße 1',
|
||||
'city' => 'Berlin',
|
||||
'zip' => '10115',
|
||||
]);
|
||||
|
||||
$expected = 'max mustermann|musterstraße 1|berlin|10115';
|
||||
$this->assertSame($expected, $customer->getMatchingKey());
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue