Compare commits
19 Commits
a05d15d17f
...
feature/sh
| Author | SHA1 | Date | |
|---|---|---|---|
| f3e226130f | |||
| 77dd0e2715 | |||
| 917f2dff09 | |||
| 6588d9f1a8 | |||
| 5d5ee7d2a5 | |||
| df19655144 | |||
| 0303803f75 | |||
| 94db48467e | |||
| 793e7be756 | |||
| 3b6d6bf28b | |||
| a9d9854e5e | |||
| 4bdd3effcc | |||
| 794acc4814 | |||
| d42896e956 | |||
| 0fe38df019 | |||
| a4bd9263a2 | |||
| 85cd1ad04f | |||
| 370834856b | |||
| ad054127aa |
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
namespace App\Domains\Commerce\Purchase\Controllers;
|
namespace App\Domains\Commerce\Purchase\Controllers;
|
||||||
|
|
||||||
use App\Shared\Integration\Services\TelepagosIntegrationService;
|
|
||||||
use App\Domains\Commerce\Purchase\Models\Purchase;
|
use App\Domains\Commerce\Purchase\Models\Purchase;
|
||||||
use App\Domains\Commerce\Purchase\Requests\PaymentIntentRequest;
|
use App\Domains\Commerce\Purchase\Requests\PaymentIntentRequest;
|
||||||
use App\Domains\Commerce\Purchase\Requests\StartCheckoutRequest;
|
use App\Domains\Commerce\Purchase\Requests\StartCheckoutRequest;
|
||||||
@@ -13,6 +12,7 @@ use App\Domains\Commerce\Purchase\Services\CheckoutService;
|
|||||||
use App\Domains\Commerce\Purchase\Services\PurchaseStateGuard;
|
use App\Domains\Commerce\Purchase\Services\PurchaseStateGuard;
|
||||||
use App\Domains\Core\Tenant\Models\Tenant;
|
use App\Domains\Core\Tenant\Models\Tenant;
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Shared\Integration\Services\TelepagosIntegrationService;
|
||||||
use Illuminate\Http\JsonResponse;
|
use Illuminate\Http\JsonResponse;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
@@ -151,11 +151,14 @@ class PurchaseController extends Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
$compra->refresh();
|
$compra->refresh();
|
||||||
|
$compra->loadMissing('event');
|
||||||
$totalAmount = (float) $compra->total;
|
$totalAmount = (float) $compra->total;
|
||||||
|
|
||||||
if ($method === 'transfer') {
|
if ($method === 'transfer') {
|
||||||
$telepagosService = new TelepagosIntegrationService;
|
$telepagosService = new TelepagosIntegrationService;
|
||||||
$telepagosService->forTenant($tenant->codigo);
|
$compra->event
|
||||||
|
? $telepagosService->forEvent($compra->event)
|
||||||
|
: $telepagosService->forTenant($tenant->codigo);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$accountInfo = $telepagosService->getAccountInfo();
|
$accountInfo = $telepagosService->getAccountInfo();
|
||||||
@@ -191,7 +194,9 @@ class PurchaseController extends Controller
|
|||||||
|
|
||||||
if ($method === 'qr') {
|
if ($method === 'qr') {
|
||||||
$telepagosService = new TelepagosIntegrationService;
|
$telepagosService = new TelepagosIntegrationService;
|
||||||
$telepagosService->forTenant($tenant->codigo);
|
$compra->event
|
||||||
|
? $telepagosService->forEvent($compra->event)
|
||||||
|
: $telepagosService->forTenant($tenant->codigo);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Log::channel('telepagos')->info('Generating Telepagos QR.', [
|
Log::channel('telepagos')->info('Generating Telepagos QR.', [
|
||||||
|
|||||||
@@ -2,13 +2,14 @@
|
|||||||
|
|
||||||
namespace App\Domains\Commerce\Purchase\Models;
|
namespace App\Domains\Commerce\Purchase\Models;
|
||||||
|
|
||||||
use App\Domains\Core\Auth\Models\User;
|
|
||||||
use App\Domains\Commerce\Cart\Models\Cart;
|
use App\Domains\Commerce\Cart\Models\Cart;
|
||||||
use App\Domains\Commerce\Catalog\Models\StockReservation;
|
use App\Domains\Commerce\Catalog\Models\StockReservation;
|
||||||
use App\Shared\Logging\Models\Concerns\LogsValueChanges;
|
|
||||||
use App\Domains\Commerce\Purchase\Events\PurchasePaid;
|
use App\Domains\Commerce\Purchase\Events\PurchasePaid;
|
||||||
|
use App\Domains\Core\Auth\Models\User;
|
||||||
use App\Domains\Core\Tenant\Models\Tenant;
|
use App\Domains\Core\Tenant\Models\Tenant;
|
||||||
|
use App\Domains\Ticketing\Event\Models\Event;
|
||||||
use App\Domains\Ticketing\Ticket\Models\Ticket;
|
use App\Domains\Ticketing\Ticket\Models\Ticket;
|
||||||
|
use App\Shared\Logging\Models\Concerns\LogsValueChanges;
|
||||||
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
@@ -21,6 +22,7 @@ use Illuminate\Support\Facades\DB;
|
|||||||
#[Fillable([
|
#[Fillable([
|
||||||
'cart_id',
|
'cart_id',
|
||||||
'stock_reservation_id',
|
'stock_reservation_id',
|
||||||
|
'event_id',
|
||||||
'tenant_codigo',
|
'tenant_codigo',
|
||||||
'user_id',
|
'user_id',
|
||||||
'status',
|
'status',
|
||||||
@@ -149,6 +151,7 @@ class Purchase extends Model
|
|||||||
return [
|
return [
|
||||||
'cart_id' => 'integer',
|
'cart_id' => 'integer',
|
||||||
'stock_reservation_id' => 'integer',
|
'stock_reservation_id' => 'integer',
|
||||||
|
'event_id' => 'integer',
|
||||||
'user_id' => 'integer',
|
'user_id' => 'integer',
|
||||||
'total' => 'decimal:2',
|
'total' => 'decimal:2',
|
||||||
];
|
];
|
||||||
@@ -162,6 +165,12 @@ class Purchase extends Model
|
|||||||
return $this->belongsTo(Tenant::class, 'tenant_codigo', 'codigo');
|
return $this->belongsTo(Tenant::class, 'tenant_codigo', 'codigo');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @return BelongsTo<Event, $this> */
|
||||||
|
public function event(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Event::class);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return BelongsTo<User, $this>
|
* @return BelongsTo<User, $this>
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -36,11 +36,11 @@ class PurchaseResource extends JsonResource
|
|||||||
=== StorefrontWebsiteType::CHECKOUT_SUMMARY_EVENT;
|
=== StorefrontWebsiteType::CHECKOUT_SUMMARY_EVENT;
|
||||||
/** @var Event|null $checkoutEvent */
|
/** @var Event|null $checkoutEvent */
|
||||||
$checkoutEvent = $usesEventSummary
|
$checkoutEvent = $usesEventSummary
|
||||||
? $items
|
? ($this->event ?? $items
|
||||||
->map(fn (PurchaseItem $item) => $item->sourceCatalogItem?->event)
|
->map(fn (PurchaseItem $item) => $item->sourceCatalogItem?->event)
|
||||||
->filter()
|
->filter()
|
||||||
->unique('id')
|
->unique('id')
|
||||||
->first()
|
->first())
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
$subtotal = $items->isNotEmpty()
|
$subtotal = $items->isNotEmpty()
|
||||||
@@ -62,6 +62,7 @@ class PurchaseResource extends JsonResource
|
|||||||
return [
|
return [
|
||||||
'id' => $this->id,
|
'id' => $this->id,
|
||||||
'cart_id' => $this->cart_id,
|
'cart_id' => $this->cart_id,
|
||||||
|
'event_id' => $this->event_id,
|
||||||
'tenant_codigo' => $this->tenant_codigo,
|
'tenant_codigo' => $this->tenant_codigo,
|
||||||
'user_id' => $this->user_id,
|
'user_id' => $this->user_id,
|
||||||
'created_at' => $this->created_at,
|
'created_at' => $this->created_at,
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ class PurchaseResponseLoader
|
|||||||
=== StorefrontWebsiteType::CHECKOUT_SUMMARY_EVENT;
|
=== StorefrontWebsiteType::CHECKOUT_SUMMARY_EVENT;
|
||||||
$relations = [
|
$relations = [
|
||||||
'tenant',
|
'tenant',
|
||||||
|
'event.attachment',
|
||||||
'items.sourceCatalogItem.event.attachment',
|
'items.sourceCatalogItem.event.attachment',
|
||||||
'stockReservation',
|
'stockReservation',
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -206,6 +206,7 @@ class StartCheckoutService
|
|||||||
fn (array $line): float => $line['selection']->getPrice() * $line['quantity'],
|
fn (array $line): float => $line['selection']->getPrice() * $line['quantity'],
|
||||||
),
|
),
|
||||||
$cart->getKey(),
|
$cart->getKey(),
|
||||||
|
$cartItems->first()?->catalogItem?->event_id,
|
||||||
);
|
);
|
||||||
$cart->update(['current_purchase_id' => $purchase->getKey()]);
|
$cart->update(['current_purchase_id' => $purchase->getKey()]);
|
||||||
$this->reservations->attachToPurchase($cart, $purchase, $this->checkoutExpiration());
|
$this->reservations->attachToPurchase($cart, $purchase, $this->checkoutExpiration());
|
||||||
@@ -273,6 +274,7 @@ class StartCheckoutService
|
|||||||
$purchaseData,
|
$purchaseData,
|
||||||
$cart->getTotalAmount(),
|
$cart->getTotalAmount(),
|
||||||
$cart->getKey(),
|
$cart->getKey(),
|
||||||
|
$cartItems->first()?->catalogItem?->event_id,
|
||||||
);
|
);
|
||||||
$cart->update(['current_purchase_id' => $purchase->getKey()]);
|
$cart->update(['current_purchase_id' => $purchase->getKey()]);
|
||||||
$this->reservations->attachToPurchase($cart, $purchase, $this->checkoutExpiration());
|
$this->reservations->attachToPurchase($cart, $purchase, $this->checkoutExpiration());
|
||||||
@@ -410,10 +412,12 @@ class StartCheckoutService
|
|||||||
array $purchaseData,
|
array $purchaseData,
|
||||||
float $total,
|
float $total,
|
||||||
?int $cartId,
|
?int $cartId,
|
||||||
|
?int $eventId,
|
||||||
): Purchase {
|
): Purchase {
|
||||||
return Purchase::query()->create([
|
return Purchase::query()->create([
|
||||||
...$purchaseData,
|
...$purchaseData,
|
||||||
'cart_id' => $cartId,
|
'cart_id' => $cartId,
|
||||||
|
'event_id' => $eventId,
|
||||||
'tenant_codigo' => $tenant->codigo,
|
'tenant_codigo' => $tenant->codigo,
|
||||||
'user_id' => $userId,
|
'user_id' => $userId,
|
||||||
'status' => Purchase::STATUS_CREATED,
|
'status' => Purchase::STATUS_CREATED,
|
||||||
|
|||||||
47
app/Domains/Core/Address/Models/Address.php
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domains\Core\Address\Models;
|
||||||
|
|
||||||
|
use App\Domains\Core\Tenant\Models\Tenant;
|
||||||
|
use App\Domains\Ticketing\Event\Models\Event;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
|
|
||||||
|
class Address extends Model
|
||||||
|
{
|
||||||
|
protected $fillable = ['label', 'address_text', 'latitude', 'longitude'];
|
||||||
|
|
||||||
|
protected function casts(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'latitude' => 'decimal:7',
|
||||||
|
'longitude' => 'decimal:7',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return array{latitude: float, longitude: float}|null */
|
||||||
|
public function coordinates(): ?array
|
||||||
|
{
|
||||||
|
if ($this->latitude === null || $this->longitude === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ['latitude' => (float) $this->latitude, 'longitude' => (float) $this->longitude];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return HasMany<Event, $this> */
|
||||||
|
public function events(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(Event::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return BelongsToMany<Tenant, $this> */
|
||||||
|
public function tenants(): BelongsToMany
|
||||||
|
{
|
||||||
|
return $this->belongsToMany(Tenant::class, 'tenant_addresses', 'address_id', 'tenant_codigo', 'id', 'codigo')
|
||||||
|
->using(TenantAddress::class)
|
||||||
|
->withPivot(['id', 'is_main'])
|
||||||
|
->withTimestamps();
|
||||||
|
}
|
||||||
|
}
|
||||||
33
app/Domains/Core/Address/Models/TenantAddress.php
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domains\Core\Address\Models;
|
||||||
|
|
||||||
|
use App\Domains\Core\Tenant\Models\Tenant;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\Pivot;
|
||||||
|
|
||||||
|
class TenantAddress extends Pivot
|
||||||
|
{
|
||||||
|
protected $table = 'tenant_addresses';
|
||||||
|
|
||||||
|
public $incrementing = true;
|
||||||
|
|
||||||
|
protected $fillable = ['tenant_codigo', 'address_id', 'is_main'];
|
||||||
|
|
||||||
|
protected function casts(): array
|
||||||
|
{
|
||||||
|
return ['is_main' => 'boolean'];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return BelongsTo<Tenant, $this> */
|
||||||
|
public function tenant(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Tenant::class, 'tenant_codigo', 'codigo');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return BelongsTo<Address, $this> */
|
||||||
|
public function address(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Address::class);
|
||||||
|
}
|
||||||
|
}
|
||||||
22
app/Domains/Core/Address/Resources/AddressResource.php
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domains\Core\Address\Resources;
|
||||||
|
|
||||||
|
use App\Domains\Core\Address\Models\Address;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Http\Resources\Json\JsonResource;
|
||||||
|
|
||||||
|
/** @mixin Address */
|
||||||
|
class AddressResource extends JsonResource
|
||||||
|
{
|
||||||
|
public function toArray(Request $request): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'id' => $this->id,
|
||||||
|
'label' => $this->label,
|
||||||
|
'address_text' => $this->address_text,
|
||||||
|
'latitude' => $this->latitude === null ? null : (float) $this->latitude,
|
||||||
|
'longitude' => $this->longitude === null ? null : (float) $this->longitude,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,6 +11,9 @@ class AdminAppContextService
|
|||||||
{
|
{
|
||||||
$tenant = $user->tenant()
|
$tenant = $user->tenant()
|
||||||
->with([
|
->with([
|
||||||
|
'socialMedia',
|
||||||
|
'addresses',
|
||||||
|
'phoneNumbers',
|
||||||
'menues' => fn ($query) => $query
|
'menues' => fn ($query) => $query
|
||||||
->whereHas(
|
->whereHas(
|
||||||
'roles',
|
'roles',
|
||||||
|
|||||||
@@ -33,7 +33,11 @@ class TenantBootstrapService
|
|||||||
$tenant,
|
$tenant,
|
||||||
[
|
[
|
||||||
'eventDateChanges',
|
'eventDateChanges',
|
||||||
'menues' => fn ($query) => $query->whereHas(
|
'menues' => fn ($query) => $query
|
||||||
|
->where(fn ($query) => $query
|
||||||
|
->where('menues.code', 'help.contact')
|
||||||
|
->orWhere('tenants_menues.is_enabled', true))
|
||||||
|
->whereHas(
|
||||||
'roles',
|
'roles',
|
||||||
fn ($query) => $query->where('codigo', RoleCode::User->value)
|
fn ($query) => $query->where('codigo', RoleCode::User->value)
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -2,8 +2,9 @@
|
|||||||
|
|
||||||
namespace App\Domains\Core\Client\Models;
|
namespace App\Domains\Core\Client\Models;
|
||||||
|
|
||||||
use App\Shared\Integration\Models\ClientIntegration;
|
|
||||||
use App\Domains\Core\Tenant\Models\Tenant;
|
use App\Domains\Core\Tenant\Models\Tenant;
|
||||||
|
use App\Domains\Ticketing\Event\Models\Event;
|
||||||
|
use App\Shared\Integration\Models\ClientIntegration;
|
||||||
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
@@ -22,6 +23,12 @@ class Client extends Model
|
|||||||
return $this->hasMany(Tenant::class);
|
return $this->hasMany(Tenant::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @return HasMany<Event, $this> */
|
||||||
|
public function events(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(Event::class);
|
||||||
|
}
|
||||||
|
|
||||||
/** @return HasMany<ClientIntegration, $this> */
|
/** @return HasMany<ClientIntegration, $this> */
|
||||||
public function integrations(): HasMany
|
public function integrations(): HasMany
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ class Menu extends Model
|
|||||||
'codigo'
|
'codigo'
|
||||||
)
|
)
|
||||||
->using(TenantMenu::class)
|
->using(TenantMenu::class)
|
||||||
->withPivot('static_content')
|
->withPivot('static_content', 'is_enabled')
|
||||||
->withTimestamps();
|
->withTimestamps();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,12 +16,14 @@ class TenantMenu extends Pivot
|
|||||||
'tenant_code',
|
'tenant_code',
|
||||||
'menu_code',
|
'menu_code',
|
||||||
'static_content',
|
'static_content',
|
||||||
|
'is_enabled',
|
||||||
];
|
];
|
||||||
|
|
||||||
protected function casts(): array
|
protected function casts(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'static_content' => 'array',
|
'static_content' => 'array',
|
||||||
|
'is_enabled' => 'boolean',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
61
app/Domains/Core/PhoneNumber/Models/PhoneNumber.php
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domains\Core\PhoneNumber\Models;
|
||||||
|
|
||||||
|
use App\Domains\Core\Tenant\Models\Tenant;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||||
|
|
||||||
|
class PhoneNumber extends Model
|
||||||
|
{
|
||||||
|
protected $attributes = ['country_code' => '+54', 'mobile_prefix' => '9'];
|
||||||
|
|
||||||
|
protected $fillable = ['label', 'country_code', 'mobile_prefix', 'area_code', 'local_number'];
|
||||||
|
|
||||||
|
public function getNumberAttribute(?string $legacyNumber): string
|
||||||
|
{
|
||||||
|
if ($this->local_number === null) {
|
||||||
|
return $legacyNumber ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
$areaCode = $this->mobile_prefix && $this->area_code
|
||||||
|
? '('.$this->area_code.')'
|
||||||
|
: $this->area_code;
|
||||||
|
|
||||||
|
return implode(' ', array_filter([
|
||||||
|
$this->country_code,
|
||||||
|
$this->mobile_prefix,
|
||||||
|
$areaCode,
|
||||||
|
$this->local_number,
|
||||||
|
], fn ($part): bool => $part !== null && $part !== ''));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function dialableNumber(string $number): string
|
||||||
|
{
|
||||||
|
return preg_replace('/[^0-9+]/', '', $number);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function telUrl(): string
|
||||||
|
{
|
||||||
|
if ($this->country_code === null || $this->local_number === null) {
|
||||||
|
return 'tel:'.self::dialableNumber($this->number);
|
||||||
|
}
|
||||||
|
|
||||||
|
$areaCode = $this->area_code ?? '';
|
||||||
|
if ($this->country_code === '+54') {
|
||||||
|
$areaCode = ltrim($areaCode, '0');
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'tel:'.self::dialableNumber(
|
||||||
|
$this->country_code.($this->mobile_prefix ?? '').$areaCode.$this->local_number
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function tenants(): BelongsToMany
|
||||||
|
{
|
||||||
|
return $this->belongsToMany(Tenant::class, 'tenant_phone_numbers', 'phone_number_id', 'tenant_codigo', 'id', 'codigo')
|
||||||
|
->using(TenantPhoneNumber::class)
|
||||||
|
->withPivot(['id', 'is_main'])
|
||||||
|
->withTimestamps();
|
||||||
|
}
|
||||||
|
}
|
||||||
31
app/Domains/Core/PhoneNumber/Models/TenantPhoneNumber.php
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domains\Core\PhoneNumber\Models;
|
||||||
|
|
||||||
|
use App\Domains\Core\Tenant\Models\Tenant;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\Pivot;
|
||||||
|
|
||||||
|
class TenantPhoneNumber extends Pivot
|
||||||
|
{
|
||||||
|
protected $table = 'tenant_phone_numbers';
|
||||||
|
|
||||||
|
public $incrementing = true;
|
||||||
|
|
||||||
|
protected $fillable = ['tenant_codigo', 'phone_number_id', 'is_main'];
|
||||||
|
|
||||||
|
protected function casts(): array
|
||||||
|
{
|
||||||
|
return ['is_main' => 'boolean'];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function tenant(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Tenant::class, 'tenant_codigo', 'codigo');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function phoneNumber(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(PhoneNumber::class);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domains\Core\PhoneNumber\Resources;
|
||||||
|
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Http\Resources\Json\JsonResource;
|
||||||
|
|
||||||
|
class PhoneNumberResource extends JsonResource
|
||||||
|
{
|
||||||
|
public function toArray(Request $request): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'id' => $this->id,
|
||||||
|
'label' => $this->label,
|
||||||
|
'country_code' => $this->country_code,
|
||||||
|
'mobile_prefix' => $this->mobile_prefix,
|
||||||
|
'area_code' => $this->area_code,
|
||||||
|
'local_number' => $this->local_number,
|
||||||
|
'number' => $this->number,
|
||||||
|
'tel_url' => $this->resource->telUrl(),
|
||||||
|
'is_main' => $this->whenPivotLoaded('tenant_phone_numbers', fn () => (bool) $this->pivot->is_main),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domains\Core\Tenant\Controllers\AdminApp;
|
||||||
|
|
||||||
|
use App\Domains\Core\Tenant\Requests\AdminApp\UpdateBrandRequest;
|
||||||
|
use App\Domains\Core\Tenant\Resources\AdminApp\BrandResource;
|
||||||
|
use App\Domains\Core\Tenant\Services\BrandService;
|
||||||
|
use App\Domains\Core\Tenant\Services\TenantInformationService;
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
class BrandController extends Controller
|
||||||
|
{
|
||||||
|
public function show(Request $request, TenantInformationService $tenantInformationService): BrandResource
|
||||||
|
{
|
||||||
|
$tenant = $request->user()->tenant()->firstOrFail();
|
||||||
|
|
||||||
|
return BrandResource::make($tenantInformationService->load($tenant));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update(
|
||||||
|
UpdateBrandRequest $request,
|
||||||
|
BrandService $brandService,
|
||||||
|
TenantInformationService $tenantInformationService,
|
||||||
|
): BrandResource {
|
||||||
|
$tenant = $request->user()->tenant()->firstOrFail();
|
||||||
|
$brandService->update($tenant, $request->validated());
|
||||||
|
|
||||||
|
return BrandResource::make($tenantInformationService->load($tenant->fresh()));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domains\Core\Tenant\Controllers\AdminApp;
|
||||||
|
|
||||||
|
use App\Domains\Core\Tenant\Requests\AdminApp\UpdateHelpMenuContentRequest;
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
class HelpMenuController extends Controller
|
||||||
|
{
|
||||||
|
private const MENU_CODES = [
|
||||||
|
'help.faq',
|
||||||
|
'help.terms-and-conditions',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function index(Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
$tenant = $request->user()->tenant()->firstOrFail();
|
||||||
|
$menus = $tenant->menues()
|
||||||
|
->whereIn('menues.code', self::MENU_CODES)
|
||||||
|
->get()
|
||||||
|
->keyBy('code');
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'data' => collect(self::MENU_CODES)->mapWithKeys(
|
||||||
|
fn (string $code): array => [$code => [
|
||||||
|
'code' => $code,
|
||||||
|
'is_enabled' => (bool) ($menus->get($code)?->pivot?->is_enabled ?? false),
|
||||||
|
'static_content' => $menus->get($code)?->pivot?->static_content
|
||||||
|
?? $this->emptyContent($code),
|
||||||
|
]]
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function toggle(
|
||||||
|
Request $request,
|
||||||
|
string $menuCode,
|
||||||
|
): JsonResponse {
|
||||||
|
abort_unless(in_array($menuCode, self::MENU_CODES, true), 404);
|
||||||
|
|
||||||
|
$tenant = $request->user()->tenant()->firstOrFail();
|
||||||
|
$tenantMenu = $tenant->menues()
|
||||||
|
->where('menues.code', $menuCode)
|
||||||
|
->firstOrFail();
|
||||||
|
$isEnabled = ! $tenantMenu->pivot->is_enabled;
|
||||||
|
|
||||||
|
$tenant->menues()->updateExistingPivot($menuCode, [
|
||||||
|
'is_enabled' => $isEnabled,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'data' => [
|
||||||
|
'code' => $tenantMenu->code,
|
||||||
|
'is_enabled' => $isEnabled,
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function updateContent(
|
||||||
|
UpdateHelpMenuContentRequest $request,
|
||||||
|
string $menuCode,
|
||||||
|
): JsonResponse {
|
||||||
|
abort_unless(in_array($menuCode, self::MENU_CODES, true), 404);
|
||||||
|
|
||||||
|
$tenant = $request->user()->tenant()->firstOrFail();
|
||||||
|
$tenantMenu = $tenant->menues()
|
||||||
|
->where('menues.code', $menuCode)
|
||||||
|
->firstOrFail();
|
||||||
|
$staticContent = $request->validated('static_content');
|
||||||
|
|
||||||
|
$tenant->menues()->updateExistingPivot($menuCode, [
|
||||||
|
'static_content' => $staticContent,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'data' => [
|
||||||
|
'code' => $tenantMenu->code,
|
||||||
|
'is_enabled' => (bool) $tenantMenu->pivot->is_enabled,
|
||||||
|
'static_content' => $staticContent,
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function emptyContent(string $menuCode): array
|
||||||
|
{
|
||||||
|
return $menuCode === 'help.faq' ? [] : ['html' => ''];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,6 +6,10 @@ use App\Domains\Commerce\Catalog\Enums\GroupLayout;
|
|||||||
use App\Domains\Commerce\Catalog\Enums\ProductLayout;
|
use App\Domains\Commerce\Catalog\Enums\ProductLayout;
|
||||||
use App\Domains\Commerce\Catalog\Models\CatalogItem;
|
use App\Domains\Commerce\Catalog\Models\CatalogItem;
|
||||||
use App\Domains\Commerce\Catalog\Models\Category;
|
use App\Domains\Commerce\Catalog\Models\Category;
|
||||||
|
use App\Domains\Core\Address\Models\Address;
|
||||||
|
use App\Domains\Core\Address\Models\TenantAddress;
|
||||||
|
use App\Domains\Core\PhoneNumber\Models\PhoneNumber;
|
||||||
|
use App\Domains\Core\PhoneNumber\Models\TenantPhoneNumber;
|
||||||
use App\Domains\Core\Client\Models\Client;
|
use App\Domains\Core\Client\Models\Client;
|
||||||
use App\Domains\Core\Menu\Models\Menu;
|
use App\Domains\Core\Menu\Models\Menu;
|
||||||
use App\Domains\Core\Menu\Models\TenantMenu;
|
use App\Domains\Core\Menu\Models\TenantMenu;
|
||||||
@@ -22,6 +26,7 @@ use Illuminate\Database\Eloquent\Model;
|
|||||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\HasOneThrough;
|
||||||
use Illuminate\Support\Facades\Schema;
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
#[Fillable([
|
#[Fillable([
|
||||||
@@ -308,6 +313,39 @@ class Tenant extends Model
|
|||||||
return $this->hasMany(WebsiteExtra::class, 'website_code', 'codigo');
|
return $this->hasMany(WebsiteExtra::class, 'website_code', 'codigo');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @return BelongsToMany<Address, $this> */
|
||||||
|
public function addresses(): BelongsToMany
|
||||||
|
{
|
||||||
|
return $this->belongsToMany(Address::class, 'tenant_addresses', 'tenant_codigo', 'address_id', 'codigo', 'id')
|
||||||
|
->using(TenantAddress::class)
|
||||||
|
->withPivot(['id', 'is_main'])
|
||||||
|
->withTimestamps()
|
||||||
|
->orderByPivot('id');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return HasOneThrough<Address, TenantAddress, $this> */
|
||||||
|
public function mainAddress(): HasOneThrough
|
||||||
|
{
|
||||||
|
return $this->hasOneThrough(Address::class, TenantAddress::class, 'tenant_codigo', 'id', 'codigo', 'address_id')
|
||||||
|
->where('tenant_addresses.is_main', true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function phoneNumbers(): BelongsToMany
|
||||||
|
{
|
||||||
|
return $this->belongsToMany(PhoneNumber::class, 'tenant_phone_numbers', 'tenant_codigo', 'phone_number_id', 'codigo', 'id')
|
||||||
|
->using(TenantPhoneNumber::class)
|
||||||
|
->withPivot(['id', 'is_main'])
|
||||||
|
->withTimestamps()
|
||||||
|
->orderByPivot('is_main', 'desc')
|
||||||
|
->orderByPivot('id');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function mainPhoneNumber(): HasOneThrough
|
||||||
|
{
|
||||||
|
return $this->hasOneThrough(PhoneNumber::class, TenantPhoneNumber::class, 'tenant_codigo', 'id', 'codigo', 'phone_number_id')
|
||||||
|
->where('tenant_phone_numbers.is_main', true);
|
||||||
|
}
|
||||||
|
|
||||||
public function menues(): BelongsToMany
|
public function menues(): BelongsToMany
|
||||||
{
|
{
|
||||||
return $this->belongsToMany(
|
return $this->belongsToMany(
|
||||||
@@ -319,7 +357,7 @@ class Tenant extends Model
|
|||||||
'code'
|
'code'
|
||||||
)
|
)
|
||||||
->using(TenantMenu::class)
|
->using(TenantMenu::class)
|
||||||
->withPivot('static_content')
|
->withPivot('static_content', 'is_enabled')
|
||||||
->withTimestamps();
|
->withTimestamps();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domains\Core\Tenant\Requests\AdminApp;
|
||||||
|
|
||||||
|
use App\Shared\Rules\CroppedImageOrBase64Rule;
|
||||||
|
use App\Shared\Rules\ImageOrBase64Rule;
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
|
||||||
|
class UpdateBrandRequest extends FormRequest
|
||||||
|
{
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return array<string, mixed> */
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
$shopit = $this->user()->tenant()->firstOrFail()->storefront_website_type_code === 'shopit';
|
||||||
|
|
||||||
|
return [
|
||||||
|
'nombre' => ['required', 'string', 'max:255'],
|
||||||
|
'primary_color' => ['required', 'regex:/^#[0-9a-fA-F]{6}$/'],
|
||||||
|
'footer_bg_color' => ['required', 'regex:/^#[0-9a-fA-F]{6}$/'],
|
||||||
|
'header_logo' => ['present', 'nullable', new ImageOrBase64Rule],
|
||||||
|
'footer_logo' => ['present', 'nullable', new ImageOrBase64Rule],
|
||||||
|
'banner_images' => [$shopit ? 'present' : 'prohibited', 'array', 'max:10'],
|
||||||
|
'banner_images.*' => ['required', new CroppedImageOrBase64Rule],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domains\Core\Tenant\Requests\AdminApp;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
|
||||||
|
class UpdateHelpMenuContentRequest extends FormRequest
|
||||||
|
{
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return array<string, mixed> */
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return match ($this->route('menuCode')) {
|
||||||
|
'help.faq' => [
|
||||||
|
'static_content' => ['required', 'array'],
|
||||||
|
'static_content.*.pregunta' => ['required', 'string'],
|
||||||
|
'static_content.*.respuesta' => ['required', 'string'],
|
||||||
|
'static_content.*.is_active' => ['required', 'boolean'],
|
||||||
|
],
|
||||||
|
'help.terms-and-conditions' => [
|
||||||
|
'static_content' => ['required', 'array'],
|
||||||
|
'static_content.html' => ['present', 'nullable', 'string'],
|
||||||
|
],
|
||||||
|
default => [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
104
app/Domains/Core/Tenant/Resources/AdminApp/BrandResource.php
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domains\Core\Tenant\Resources\AdminApp;
|
||||||
|
|
||||||
|
use App\Domains\Core\Tenant\Models\Tenant;
|
||||||
|
use App\Shared\Attachable\Models\Attachment;
|
||||||
|
use App\Shared\Attachable\Models\AttachmentCrop;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Http\Resources\Json\JsonResource;
|
||||||
|
|
||||||
|
/** @mixin Tenant */
|
||||||
|
class BrandResource extends JsonResource
|
||||||
|
{
|
||||||
|
/** @return array<string, mixed> */
|
||||||
|
public function toArray(Request $request): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'nombre' => $this->nombre,
|
||||||
|
'primary_color' => $this->primary_color,
|
||||||
|
'header_bg_color' => $this->header_bg_color,
|
||||||
|
'footer_color' => $this->footerColor(),
|
||||||
|
'header_logo' => $this->headerLogo?->getTemporaryUrl(1440),
|
||||||
|
'header_logo_key' => $this->headerLogo?->key,
|
||||||
|
'footer_logo' => $this->footerLogo?->getTemporaryUrl(1440),
|
||||||
|
'footer_logo_key' => $this->footerLogo?->key,
|
||||||
|
'can_edit_carousel' => $this->canEditCarousel(),
|
||||||
|
'banner_images' => $this->bannerImages(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function footerColor(): string
|
||||||
|
{
|
||||||
|
$footerColor = strtolower(trim((string) $this->footer_bg_color));
|
||||||
|
$primaryColor = strtolower(trim((string) $this->primary_color));
|
||||||
|
|
||||||
|
if ($footerColor !== '' && $footerColor === $primaryColor) {
|
||||||
|
return 'primary';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (in_array($footerColor, ['#000', '#000000', 'black'], true)) {
|
||||||
|
return 'black';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($footerColor === '' || in_array($footerColor, ['#fff', '#ffffff', 'white'], true)) {
|
||||||
|
return 'white';
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'primary';
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return array<int, array<string, mixed>> */
|
||||||
|
private function bannerImages(): array
|
||||||
|
{
|
||||||
|
if (! $this->canEditCarousel()) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$extra = $this->websiteExtras->first(
|
||||||
|
fn ($extra) => $extra->is_enabled && $extra->websiteTypeExtra->codigo === 'carousel'
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($extra === null) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$images = $extra->resolvedConfig();
|
||||||
|
|
||||||
|
if (! is_array($images)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return collect($images)
|
||||||
|
->filter(fn ($image) => $image instanceof Attachment)
|
||||||
|
->map(function (Attachment $image): array {
|
||||||
|
$desktopCrop = $image->cropVariants
|
||||||
|
->firstWhere('variant', AttachmentCrop::DESKTOP);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'key' => $image->key,
|
||||||
|
'url' => ($desktopCrop?->croppedAttachment ?? $image)->getTemporaryUrl(1440),
|
||||||
|
'original_url' => $image->getTemporaryUrl(1440),
|
||||||
|
'crop' => $desktopCrop === null ? null : [
|
||||||
|
'crop_horizontal' => $desktopCrop->crop_horizontal,
|
||||||
|
'crop_vertical' => $desktopCrop->crop_vertical,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
})
|
||||||
|
->values()
|
||||||
|
->all();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function canEditCarousel(): bool
|
||||||
|
{
|
||||||
|
if ($this->storefront_website_type_code !== 'shopit') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$extra = $this->websiteExtras->first(
|
||||||
|
fn ($extra) => $extra->websiteTypeExtra->codigo === 'carousel'
|
||||||
|
);
|
||||||
|
|
||||||
|
return $extra === null || $extra->is_enabled;
|
||||||
|
}
|
||||||
|
}
|
||||||
14
app/Domains/Core/Tenant/Resources/TenantAddressResource.php
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domains\Core\Tenant\Resources;
|
||||||
|
|
||||||
|
use App\Domains\Core\Address\Resources\AddressResource;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
class TenantAddressResource extends AddressResource
|
||||||
|
{
|
||||||
|
public function toArray(Request $request): array
|
||||||
|
{
|
||||||
|
return [...parent::toArray($request), 'is_main' => (bool) $this->pivot->is_main];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,7 +3,9 @@
|
|||||||
namespace App\Domains\Core\Tenant\Resources;
|
namespace App\Domains\Core\Tenant\Resources;
|
||||||
|
|
||||||
use App\Domains\Commerce\Catalog\Models\Category;
|
use App\Domains\Commerce\Catalog\Models\Category;
|
||||||
|
use App\Domains\Core\Address\Resources\AddressResource;
|
||||||
use App\Domains\Core\Menu\Models\Menu;
|
use App\Domains\Core\Menu\Models\Menu;
|
||||||
|
use App\Domains\Core\PhoneNumber\Resources\PhoneNumberResource;
|
||||||
use App\Domains\Core\Tenant\Models\StorefrontWebsiteType;
|
use App\Domains\Core\Tenant\Models\StorefrontWebsiteType;
|
||||||
use App\Domains\Core\Tenant\Models\Tenant;
|
use App\Domains\Core\Tenant\Models\Tenant;
|
||||||
use App\Domains\Ticketing\Event\Models\Event;
|
use App\Domains\Ticketing\Event\Models\Event;
|
||||||
@@ -41,7 +43,11 @@ class TenantResource extends JsonResource
|
|||||||
'site_title' => $this->site_title ?? 'ShopitFront',
|
'site_title' => $this->site_title ?? 'ShopitFront',
|
||||||
'asset_url' => config('filesystems.disks.s3.url'),
|
'asset_url' => config('filesystems.disks.s3.url'),
|
||||||
'address' => $this->address,
|
'address' => $this->address,
|
||||||
|
'addresses' => $this->whenLoaded('addresses', fn () => TenantAddressResource::collection($this->addresses)),
|
||||||
|
'main_address' => $this->whenLoaded('mainAddress', fn () => AddressResource::make($this->mainAddress)),
|
||||||
'phone' => $this->phone,
|
'phone' => $this->phone,
|
||||||
|
'phone_numbers' => $this->whenLoaded('phoneNumbers', fn () => PhoneNumberResource::collection($this->phoneNumbers)),
|
||||||
|
'main_phone_number' => $this->whenLoaded('mainPhoneNumber', fn () => PhoneNumberResource::make($this->mainPhoneNumber)),
|
||||||
'favicon' => $this->favicon?->getTemporaryUrl(1440),
|
'favicon' => $this->favicon?->getTemporaryUrl(1440),
|
||||||
'primary_color' => $this->primary_color,
|
'primary_color' => $this->primary_color,
|
||||||
'secondary_color' => $this->secondary_color,
|
'secondary_color' => $this->secondary_color,
|
||||||
@@ -127,6 +133,7 @@ class TenantResource extends JsonResource
|
|||||||
'subtitle' => $event->subtitle,
|
'subtitle' => $event->subtitle,
|
||||||
'description' => $event->description,
|
'description' => $event->description,
|
||||||
'location' => $event->location,
|
'location' => $event->location,
|
||||||
|
'address' => AddressResource::make($event->address),
|
||||||
'date_text' => $event->date_text,
|
'date_text' => $event->date_text,
|
||||||
'dates' => $event->dates
|
'dates' => $event->dates
|
||||||
->filter(fn (EventDate $date): bool => $date->rescheduled_to_event_date_id === null)
|
->filter(fn (EventDate $date): bool => $date->rescheduled_to_event_date_id === null)
|
||||||
@@ -226,7 +233,7 @@ class TenantResource extends JsonResource
|
|||||||
'route' => $menu->route,
|
'route' => $menu->route,
|
||||||
];
|
];
|
||||||
|
|
||||||
if ($menu->pivot?->static_content !== null) {
|
if ($menu->content_type === Menu::CONTENT_TYPE_STATIC && $menu->pivot?->static_content !== null) {
|
||||||
$formatted['static_content'] = $menu->pivot->static_content;
|
$formatted['static_content'] = $menu->pivot->static_content;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
165
app/Domains/Core/Tenant/Services/BrandService.php
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domains\Core\Tenant\Services;
|
||||||
|
|
||||||
|
use App\Domains\Core\Tenant\Models\Tenant;
|
||||||
|
use App\Shared\Attachable\Enums\AttachmentType;
|
||||||
|
use App\Shared\Attachable\Models\Attachment;
|
||||||
|
use App\Shared\Attachable\Services\AttachmentService;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
use Illuminate\Validation\ValidationException;
|
||||||
|
use Throwable;
|
||||||
|
|
||||||
|
class BrandService
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly AttachmentService $attachmentService,
|
||||||
|
private readonly WebsiteExtraService $websiteExtraService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/** @param array<string, mixed> $data */
|
||||||
|
public function update(Tenant $tenant, array $data): void
|
||||||
|
{
|
||||||
|
$carousel = $tenant->storefront_website_type_code === 'shopit'
|
||||||
|
? $tenant->websiteExtras()->whereHas('websiteTypeExtra',
|
||||||
|
fn ($query) => $query->where('codigo', 'carousel'))->first()
|
||||||
|
: null;
|
||||||
|
$allowedBannerIds = is_array($carousel?->config) ? $carousel->config : [];
|
||||||
|
if ($carousel !== null && ! $carousel->is_enabled && ($data['banner_images'] ?? []) !== []) {
|
||||||
|
throw ValidationException::withMessages(['banner_images' => 'El carrusel está deshabilitado.']);
|
||||||
|
}
|
||||||
|
$seen = [];
|
||||||
|
|
||||||
|
foreach (['header_logo' => $tenant->header_logo_id, 'footer_logo' => $tenant->footer_logo_id] as $field => $id) {
|
||||||
|
$this->existingImage($data[$field], $id === null ? [] : [$id], $field);
|
||||||
|
}
|
||||||
|
foreach ($data['banner_images'] ?? [] as $index => $item) {
|
||||||
|
$image = is_array($item) ? $item['image'] : $item;
|
||||||
|
$attachment = $this->existingImage($image, $allowedBannerIds, "banner_images.{$index}");
|
||||||
|
if ($attachment !== null) {
|
||||||
|
if (isset($seen[$attachment->id])) {
|
||||||
|
throw ValidationException::withMessages(["banner_images.{$index}" => 'La imagen está repetida.']);
|
||||||
|
}
|
||||||
|
$seen[$attachment->id] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$created = [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
DB::transaction(function () use ($tenant, $data, $carousel, &$created): void {
|
||||||
|
$header = $this->logo($data['header_logo'], $tenant->header_logo_id, $tenant, $created);
|
||||||
|
$footer = $this->logo($data['footer_logo'], $tenant->footer_logo_id, $tenant, $created);
|
||||||
|
|
||||||
|
$tenant->update([
|
||||||
|
'nombre' => trim($data['nombre']),
|
||||||
|
'primary_color' => $data['primary_color'],
|
||||||
|
'footer_bg_color' => $data['footer_bg_color'],
|
||||||
|
'header_logo_id' => $header?->id,
|
||||||
|
'footer_logo_id' => $footer?->id,
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($tenant->storefront_website_type_code !== 'shopit'
|
||||||
|
|| ($carousel !== null && ! $carousel->is_enabled)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$definition = $this->websiteExtraService->definitionForTenant($tenant, 'carousel');
|
||||||
|
$ids = [];
|
||||||
|
$recrops = [];
|
||||||
|
foreach ($data['banner_images'] as $item) {
|
||||||
|
$image = is_array($item) ? $item['image'] : $item;
|
||||||
|
$attachment = $this->existingImage($image, $carousel?->config ?? [], 'banner_images');
|
||||||
|
$crops = $this->crops($item);
|
||||||
|
|
||||||
|
if ($attachment === null) {
|
||||||
|
$attachment = $crops === null
|
||||||
|
? $this->attachmentService->store($image, "tenants/{$tenant->codigo}/extras/carousel")
|
||||||
|
: $this->attachmentService->storeCroppedImageVariants(
|
||||||
|
$image, "tenants/{$tenant->codigo}/extras/carousel", $crops
|
||||||
|
);
|
||||||
|
$created[] = $attachment;
|
||||||
|
} elseif ($crops !== null) {
|
||||||
|
$recrops[] = [$attachment, $crops];
|
||||||
|
}
|
||||||
|
$ids[] = $attachment->id;
|
||||||
|
}
|
||||||
|
|
||||||
|
$tenant->websiteExtras()->updateOrCreate(
|
||||||
|
['website_type_extra_id' => $definition->id],
|
||||||
|
['config' => $ids]
|
||||||
|
);
|
||||||
|
|
||||||
|
foreach ($recrops as [$attachment, $crops]) {
|
||||||
|
$this->attachmentService->updateImageCropVariants($attachment, $crops);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (Throwable $error) {
|
||||||
|
foreach ($created as $attachment) {
|
||||||
|
$paths = [$attachment->path];
|
||||||
|
foreach ($attachment->cropVariants as $crop) {
|
||||||
|
if ($crop->croppedAttachment !== null) {
|
||||||
|
$paths[] = $crop->croppedAttachment->path;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Storage::disk('s3')->delete($paths);
|
||||||
|
}
|
||||||
|
throw $error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param array<int, int> $allowedIds */
|
||||||
|
private function existingImage(mixed $value, array $allowedIds, string $field): ?Attachment
|
||||||
|
{
|
||||||
|
if (! is_string($value) || ! Str::isUuid($value)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$attachment = Attachment::query()->where('key', $value)->first();
|
||||||
|
if ($attachment === null || $attachment->type !== AttachmentType::Image
|
||||||
|
|| ! in_array($attachment->id, $allowedIds, true)) {
|
||||||
|
throw ValidationException::withMessages([$field => 'La imagen no pertenece a esta marca.']);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $attachment;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param array<int, Attachment> $created */
|
||||||
|
private function logo(mixed $value, ?int $currentId, Tenant $tenant, array &$created): ?Attachment
|
||||||
|
{
|
||||||
|
if ($value === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$existing = $this->existingImage($value, $currentId === null ? [] : [$currentId], 'logo');
|
||||||
|
if ($existing !== null) {
|
||||||
|
return $existing;
|
||||||
|
}
|
||||||
|
|
||||||
|
$attachment = $this->attachmentService->store($value, "tenants/{$tenant->codigo}/logos");
|
||||||
|
$created[] = $attachment;
|
||||||
|
|
||||||
|
return $attachment;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return array<string, array<string, mixed>>|null */
|
||||||
|
private function crops(mixed $item): ?array
|
||||||
|
{
|
||||||
|
if (! is_array($item)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! isset($item['crop_horizontal'], $item['crop_vertical'])) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$crop = [
|
||||||
|
'crop_horizontal' => $item['crop_horizontal'],
|
||||||
|
'crop_vertical' => $item['crop_vertical'],
|
||||||
|
];
|
||||||
|
|
||||||
|
return ['desktop' => $crop, 'mobile' => $crop];
|
||||||
|
}
|
||||||
|
}
|
||||||
26
app/Domains/Core/Tenant/Services/TenantAddressService.php
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domains\Core\Tenant\Services;
|
||||||
|
|
||||||
|
use App\Domains\Core\Address\Models\Address;
|
||||||
|
use App\Domains\Core\Tenant\Models\Tenant;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
|
class TenantAddressService
|
||||||
|
{
|
||||||
|
public function setMainAddress(Tenant $tenant, Address $address): void
|
||||||
|
{
|
||||||
|
DB::transaction(function () use ($tenant, $address): void {
|
||||||
|
// Serialize changes for this tenant, including when it has no principal address yet.
|
||||||
|
Tenant::query()->where('codigo', $tenant->codigo)->lockForUpdate()->firstOrFail();
|
||||||
|
$tenant->addresses()->where('addresses.id', $address->id)->firstOrFail();
|
||||||
|
|
||||||
|
DB::table('tenant_addresses')->where('tenant_codigo', $tenant->codigo)
|
||||||
|
->where('is_main', true)->update(['is_main' => false, 'updated_at' => now()]);
|
||||||
|
$tenant->addresses()->updateExistingPivot($address->id, ['is_main' => true]);
|
||||||
|
});
|
||||||
|
|
||||||
|
$tenant->unsetRelation('addresses');
|
||||||
|
$tenant->unsetRelation('mainAddress');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,9 +18,14 @@ class TenantInformationService
|
|||||||
'footerBackgroundImage',
|
'footerBackgroundImage',
|
||||||
'storefrontWebsiteType',
|
'storefrontWebsiteType',
|
||||||
'socialMedia',
|
'socialMedia',
|
||||||
|
'addresses',
|
||||||
|
'mainAddress',
|
||||||
|
'phoneNumbers',
|
||||||
|
'mainPhoneNumber',
|
||||||
'websiteExtras.websiteTypeExtra',
|
'websiteExtras.websiteTypeExtra',
|
||||||
'activeEvent.dates',
|
'activeEvent.dates',
|
||||||
'activeEvent.socialMedia',
|
'activeEvent.socialMedia',
|
||||||
|
'activeEvent.address',
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ Es la raíz del modelo multi-tenant. Gestiona organizaciones/sitios, tipos de we
|
|||||||
- `StorefrontWebsiteTypeExtra`: definición de un extra y su configuración admitida.
|
- `StorefrontWebsiteTypeExtra`: definición de un extra y su configuración admitida.
|
||||||
- `WebsiteExtra`: valor resuelto y estado del extra para un tenant.
|
- `WebsiteExtra`: valor resuelto y estado del extra para un tenant.
|
||||||
- `SocialMedia`: catálogo de redes sociales asociables.
|
- `SocialMedia`: catálogo de redes sociales asociables.
|
||||||
|
- `phoneNumbers` y `mainPhoneNumber`: teléfonos públicos del tenant, definidos en el dominio `Core/PhoneNumber` y vinculados por `tenant_phone_numbers`. Solo un vínculo por tenant puede ser principal.
|
||||||
|
|
||||||
## Servicios
|
## Servicios
|
||||||
|
|
||||||
@@ -31,3 +32,5 @@ El par `(dominio, base_path)` es único. Un mismo dominio puede alojar el tenant
|
|||||||
## Dependencias y reglas
|
## Dependencias y reglas
|
||||||
|
|
||||||
Usa `Attachable` para logos y archivos. Es referenciado por casi todos los dominios para aislamiento. El `codigo` es clave de ruta y clave foránea heredada; no debe sustituirse por `id` sin una migración integral.
|
Usa `Attachable` para logos y archivos. Es referenciado por casi todos los dominios para aislamiento. El `codigo` es clave de ruta y clave foránea heredada; no debe sustituirse por `id` sin una migración integral.
|
||||||
|
|
||||||
|
Los resources exponen `phone_numbers` (incluye `is_main`) y `main_phone_number`, con `id`, `label`, `number` y `tel_url`. Contacto muestra la colección y el footer usa el principal. La migración copia primero `tenants.phone` como principal y luego el teléfono de `help.contact`, si es distinto; si no existe teléfono del tenant, el del menú pasa a ser principal. Los duplicados por formato se comparan dentro de cada tenant sin inferir país ni código de área. Los campos anteriores se conservan como datos legacy, pero la tienda consume las relaciones nuevas. No hay endpoints de edición de teléfonos ni cambios del adminapp en esta etapa.
|
||||||
|
|||||||
@@ -1,11 +1,18 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
use App\Domains\Core\Tenant\Controllers\AdminApp\BrandController;
|
||||||
|
use App\Domains\Core\Tenant\Controllers\AdminApp\HelpMenuController;
|
||||||
use App\Domains\Core\Tenant\Controllers\AdminApp\WebsiteExtraController;
|
use App\Domains\Core\Tenant\Controllers\AdminApp\WebsiteExtraController;
|
||||||
use Illuminate\Support\Facades\Route;
|
use Illuminate\Support\Facades\Route;
|
||||||
|
|
||||||
Route::prefix('v1/adminapp/tenant')
|
Route::prefix('v1/adminapp/tenant')
|
||||||
->middleware(['auth:sanctum', 'adminapp.tenant'])
|
->middleware(['auth:sanctum', 'adminapp.tenant'])
|
||||||
->group(function (): void {
|
->group(function (): void {
|
||||||
|
Route::get('brand', [BrandController::class, 'show']);
|
||||||
|
Route::put('brand', [BrandController::class, 'update']);
|
||||||
|
Route::get('help-menus', [HelpMenuController::class, 'index']);
|
||||||
|
Route::patch('help-menus/{menuCode}/toggle', [HelpMenuController::class, 'toggle']);
|
||||||
|
Route::put('help-menus/{menuCode}/content', [HelpMenuController::class, 'updateContent']);
|
||||||
Route::get('website-extras', [WebsiteExtraController::class, 'show']);
|
Route::get('website-extras', [WebsiteExtraController::class, 'show']);
|
||||||
Route::get('website-extras/{websiteExtraCode}', [WebsiteExtraController::class, 'showExtra']);
|
Route::get('website-extras/{websiteExtraCode}', [WebsiteExtraController::class, 'showExtra']);
|
||||||
Route::put('website-extras/{websiteExtraCode}', [WebsiteExtraController::class, 'update']);
|
Route::put('website-extras/{websiteExtraCode}', [WebsiteExtraController::class, 'update']);
|
||||||
|
|||||||
@@ -5,28 +5,28 @@ use App\Domains\Ticketing\Desfile\Controllers\EntryReservationController;
|
|||||||
use Illuminate\Support\Facades\Route;
|
use Illuminate\Support\Facades\Route;
|
||||||
|
|
||||||
Route::get('v1/adminapp/tenant/desfile/entry-reservations', [EntryReservationController::class, 'index'])
|
Route::get('v1/adminapp/tenant/desfile/entry-reservations', [EntryReservationController::class, 'index'])
|
||||||
->middleware(['auth:sanctum', 'adminapp.tenant', 'tenant.menu:adminapp.desfile.reservas'])
|
->middleware(['auth:sanctum', 'adminapp.tenant', 'tenant.menu:onticket.adminapp.desfile.reservas'])
|
||||||
->name('adminapp.desfile.entry-reservations.index');
|
->name('adminapp.desfile.entry-reservations.index');
|
||||||
Route::get('v1/adminapp/tenant/desfile/entry-reservations/pdf', [EntryReservationController::class, 'downloadPdf'])
|
Route::get('v1/adminapp/tenant/desfile/entry-reservations/pdf', [EntryReservationController::class, 'downloadPdf'])
|
||||||
->middleware(['auth:sanctum', 'adminapp.tenant', 'tenant.menu:adminapp.desfile.reservas'])
|
->middleware(['auth:sanctum', 'adminapp.tenant', 'tenant.menu:onticket.adminapp.desfile.reservas'])
|
||||||
->name('adminapp.desfile.entry-reservations.pdf');
|
->name('adminapp.desfile.entry-reservations.pdf');
|
||||||
Route::get('v1/adminapp/tenant/desfile/entry-reservations/excel', [EntryReservationController::class, 'downloadExcel'])
|
Route::get('v1/adminapp/tenant/desfile/entry-reservations/excel', [EntryReservationController::class, 'downloadExcel'])
|
||||||
->middleware(['auth:sanctum', 'adminapp.tenant', 'tenant.menu:adminapp.desfile.reservas'])
|
->middleware(['auth:sanctum', 'adminapp.tenant', 'tenant.menu:onticket.adminapp.desfile.reservas'])
|
||||||
->name('adminapp.desfile.entry-reservations.excel');
|
->name('adminapp.desfile.entry-reservations.excel');
|
||||||
Route::get('v1/adminapp/tenant/desfile/entry-reservations/{reservation}/ticket/pdf', [EntryReservationController::class, 'downloadTicketPdf'])
|
Route::get('v1/adminapp/tenant/desfile/entry-reservations/{reservation}/ticket/pdf', [EntryReservationController::class, 'downloadTicketPdf'])
|
||||||
->whereNumber('reservation')
|
->whereNumber('reservation')
|
||||||
->middleware(['auth:sanctum', 'adminapp.tenant', 'tenant.menu:adminapp.desfile.reservas'])
|
->middleware(['auth:sanctum', 'adminapp.tenant', 'tenant.menu:onticket.adminapp.desfile.reservas'])
|
||||||
->name('adminapp.desfile.entry-reservations.ticket.pdf');
|
->name('adminapp.desfile.entry-reservations.ticket.pdf');
|
||||||
Route::post('v1/adminapp/tenant/desfile/entry-reservations', [EntryReservationController::class, 'store'])
|
Route::post('v1/adminapp/tenant/desfile/entry-reservations', [EntryReservationController::class, 'store'])
|
||||||
->middleware(['auth:sanctum', 'adminapp.tenant', 'tenant.menu:adminapp.desfile.reservas'])
|
->middleware(['auth:sanctum', 'adminapp.tenant', 'tenant.menu:onticket.adminapp.desfile.reservas'])
|
||||||
->name('adminapp.desfile.entry-reservations.store');
|
->name('adminapp.desfile.entry-reservations.store');
|
||||||
Route::delete('v1/adminapp/tenant/desfile/entry-reservations/{reservation}', [EntryReservationController::class, 'destroy'])
|
Route::delete('v1/adminapp/tenant/desfile/entry-reservations/{reservation}', [EntryReservationController::class, 'destroy'])
|
||||||
->whereNumber('reservation')
|
->whereNumber('reservation')
|
||||||
->middleware(['auth:sanctum', 'adminapp.tenant', 'tenant.menu:adminapp.desfile.reservas'])
|
->middleware(['auth:sanctum', 'adminapp.tenant', 'tenant.menu:onticket.adminapp.desfile.reservas'])
|
||||||
->name('adminapp.desfile.entry-reservations.destroy');
|
->name('adminapp.desfile.entry-reservations.destroy');
|
||||||
|
|
||||||
Route::prefix('v1/adminapp/tenant/desfile')
|
Route::prefix('v1/adminapp/tenant/desfile')
|
||||||
->middleware(['auth:sanctum', 'adminapp.tenant', 'tenant.menu:adminapp.desfile.entradas'])
|
->middleware(['auth:sanctum', 'adminapp.tenant', 'tenant.menu:onticket.adminapp.desfile.entradas'])
|
||||||
->group(function (): void {
|
->group(function (): void {
|
||||||
Route::get('entries', [EntryController::class, 'show'])
|
Route::get('entries', [EntryController::class, 'show'])
|
||||||
->name('adminapp.desfile.entries.show');
|
->name('adminapp.desfile.entries.show');
|
||||||
|
|||||||
@@ -3,6 +3,8 @@
|
|||||||
namespace App\Domains\Ticketing\Event\Models;
|
namespace App\Domains\Ticketing\Event\Models;
|
||||||
|
|
||||||
use App\Domains\Commerce\Catalog\Models\CatalogItem;
|
use App\Domains\Commerce\Catalog\Models\CatalogItem;
|
||||||
|
use App\Domains\Core\Address\Models\Address;
|
||||||
|
use App\Domains\Core\Client\Models\Client;
|
||||||
use App\Domains\Core\Tenant\Models\SocialMedia;
|
use App\Domains\Core\Tenant\Models\SocialMedia;
|
||||||
use App\Domains\Core\Tenant\Models\Tenant;
|
use App\Domains\Core\Tenant\Models\Tenant;
|
||||||
use App\Domains\Ticketing\Ticket\Models\Ticket;
|
use App\Domains\Ticketing\Ticket\Models\Ticket;
|
||||||
@@ -14,14 +16,14 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
|||||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
|
|
||||||
#[Fillable(['tenant_code', 'slug', 'event_category_id', 'title', 'subtitle', 'description', 'location', 'exact_location', 'date_text', 'published_at', 'attachment_id'])]
|
#[Fillable(['client_id', 'tenant_code', 'slug', 'event_category_id', 'title', 'subtitle', 'description', 'address_id', 'location', 'exact_location', 'date_text', 'published_at', 'attachment_id'])]
|
||||||
class Event extends Model
|
class Event extends Model
|
||||||
{
|
{
|
||||||
use HasFactory;
|
use HasFactory;
|
||||||
|
|
||||||
protected function casts(): array
|
protected function casts(): array
|
||||||
{
|
{
|
||||||
return ['published_at' => 'datetime', 'exact_location' => 'array'];
|
return ['client_id' => 'integer', 'address_id' => 'integer', 'published_at' => 'datetime', 'exact_location' => 'array'];
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @return BelongsTo<Tenant, $this> */
|
/** @return BelongsTo<Tenant, $this> */
|
||||||
@@ -30,6 +32,23 @@ class Event extends Model
|
|||||||
return $this->belongsTo(Tenant::class, 'tenant_code', 'codigo');
|
return $this->belongsTo(Tenant::class, 'tenant_code', 'codigo');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @return BelongsTo<Client, $this> */
|
||||||
|
public function client(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Client::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return BelongsTo<Address, $this> */
|
||||||
|
public function address(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Address::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function effectiveClient(): Client
|
||||||
|
{
|
||||||
|
return $this->client ?? $this->tenant->client;
|
||||||
|
}
|
||||||
|
|
||||||
/** @return BelongsTo<EventCategory, $this> */
|
/** @return BelongsTo<EventCategory, $this> */
|
||||||
public function eventCategory(): BelongsTo
|
public function eventCategory(): BelongsTo
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace App\Domains\Ticketing\Event\Resources;
|
namespace App\Domains\Ticketing\Event\Resources;
|
||||||
|
|
||||||
|
use App\Domains\Core\Address\Resources\AddressResource;
|
||||||
use App\Domains\Ticketing\Event\Models\Event;
|
use App\Domains\Ticketing\Event\Models\Event;
|
||||||
use App\Domains\Ticketing\Event\Services\EventDateGroupingService;
|
use App\Domains\Ticketing\Event\Services\EventDateGroupingService;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
@@ -21,6 +22,7 @@ class EventResource extends JsonResource
|
|||||||
'title' => $this->title,
|
'title' => $this->title,
|
||||||
'subtitle' => $this->subtitle,
|
'subtitle' => $this->subtitle,
|
||||||
'description' => $this->description,
|
'description' => $this->description,
|
||||||
|
'address' => AddressResource::make($this->address),
|
||||||
'location' => $this->location,
|
'location' => $this->location,
|
||||||
'exact_location' => $this->exact_location,
|
'exact_location' => $this->exact_location,
|
||||||
'date_text' => $this->date_text,
|
'date_text' => $this->date_text,
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ namespace App\Domains\Ticketing\Event\Resources;
|
|||||||
use App\Domains\Commerce\Catalog\Enums\InventoryPolicy;
|
use App\Domains\Commerce\Catalog\Enums\InventoryPolicy;
|
||||||
use App\Domains\Commerce\Catalog\Models\CatalogItem;
|
use App\Domains\Commerce\Catalog\Models\CatalogItem;
|
||||||
use App\Domains\Commerce\Catalog\Models\Variant;
|
use App\Domains\Commerce\Catalog\Models\Variant;
|
||||||
|
use App\Domains\Core\Address\Resources\AddressResource;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Http\Resources\Json\JsonResource;
|
use Illuminate\Http\Resources\Json\JsonResource;
|
||||||
|
|
||||||
@@ -19,6 +20,7 @@ class PublicEventResource extends JsonResource
|
|||||||
'title' => $this->title,
|
'title' => $this->title,
|
||||||
'subtitle' => $this->subtitle,
|
'subtitle' => $this->subtitle,
|
||||||
'description' => $this->description,
|
'description' => $this->description,
|
||||||
|
'address' => AddressResource::make($this->address),
|
||||||
'location' => $this->location,
|
'location' => $this->location,
|
||||||
'exact_location' => $this->exact_location,
|
'exact_location' => $this->exact_location,
|
||||||
'date_text' => $this->date_text,
|
'date_text' => $this->date_text,
|
||||||
|
|||||||
37
app/Domains/Ticketing/Event/Services/EventAddressService.php
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domains\Ticketing\Event\Services;
|
||||||
|
|
||||||
|
use App\Domains\Core\Address\Models\Address;
|
||||||
|
use App\Domains\Ticketing\Event\Models\Event;
|
||||||
|
|
||||||
|
class EventAddressService
|
||||||
|
{
|
||||||
|
/** Called inside the event update transaction, after validating the legacy location fields. */
|
||||||
|
public function syncLocation(Event $event, bool $coordinatesProvided = false): void
|
||||||
|
{
|
||||||
|
$address = $event->address;
|
||||||
|
$coordinates = $coordinatesProvided || $address === null ? $event->exact_location : $address->coordinates();
|
||||||
|
$values = [
|
||||||
|
'address_text' => $event->location,
|
||||||
|
'latitude' => $coordinates['latitude'] ?? null,
|
||||||
|
'longitude' => $coordinates['longitude'] ?? null,
|
||||||
|
];
|
||||||
|
|
||||||
|
if ($address !== null && ! (clone $address)->fill($values)->isDirty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Editing one event must not change the location of another event or a tenant.
|
||||||
|
if ($address === null || $address->tenants()->exists()
|
||||||
|
|| $address->events()->where('events.id', '!=', $event->id)->exists()) {
|
||||||
|
$address = Address::create(['label' => $address?->label ?? 'Ubicación del evento', ...$values]);
|
||||||
|
$event->address()->associate($address);
|
||||||
|
$event->save();
|
||||||
|
} else {
|
||||||
|
$address->update($values);
|
||||||
|
}
|
||||||
|
|
||||||
|
$event->setRelation('address', $address);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,18 +2,18 @@
|
|||||||
|
|
||||||
namespace App\Domains\Ticketing\Event\Services;
|
namespace App\Domains\Ticketing\Event\Services;
|
||||||
|
|
||||||
use App\Domains\Core\Auth\Models\User;
|
|
||||||
use App\Domains\Commerce\Cart\Services\InvalidateEventDateCartsService;
|
use App\Domains\Commerce\Cart\Services\InvalidateEventDateCartsService;
|
||||||
use App\Domains\Commerce\Catalog\Models\Variant;
|
use App\Domains\Commerce\Catalog\Models\Variant;
|
||||||
use App\Domains\Commerce\Catalog\Services\StockReservationService;
|
use App\Domains\Commerce\Catalog\Services\StockReservationService;
|
||||||
use App\Domains\Commerce\Catalog\Services\VariantReplacementService;
|
use App\Domains\Commerce\Catalog\Services\VariantReplacementService;
|
||||||
|
use App\Domains\Core\Auth\Models\User;
|
||||||
|
use App\Domains\Core\Tenant\Models\Tenant;
|
||||||
use App\Domains\Ticketing\Event\Enums\EventDateChangeType;
|
use App\Domains\Ticketing\Event\Enums\EventDateChangeType;
|
||||||
use App\Domains\Ticketing\Event\Events\EventDateRescheduled;
|
use App\Domains\Ticketing\Event\Events\EventDateRescheduled;
|
||||||
use App\Domains\Ticketing\Event\Events\EventDateSuspended;
|
use App\Domains\Ticketing\Event\Events\EventDateSuspended;
|
||||||
|
use App\Domains\Ticketing\Event\Models\Event;
|
||||||
use App\Domains\Ticketing\Event\Models\EventDate;
|
use App\Domains\Ticketing\Event\Models\EventDate;
|
||||||
use App\Domains\Ticketing\Event\Models\EventDateChange;
|
use App\Domains\Ticketing\Event\Models\EventDateChange;
|
||||||
use App\Domains\Ticketing\Event\Models\Event;
|
|
||||||
use App\Domains\Core\Tenant\Models\Tenant;
|
|
||||||
use App\Domains\Ticketing\Ticket\Models\Ticket;
|
use App\Domains\Ticketing\Ticket\Models\Ticket;
|
||||||
use Illuminate\Support\Collection;
|
use Illuminate\Support\Collection;
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
@@ -32,11 +32,12 @@ class EventService
|
|||||||
private readonly AffectedEventDatePurchaseResolver $affectedPurchaseResolver,
|
private readonly AffectedEventDatePurchaseResolver $affectedPurchaseResolver,
|
||||||
private readonly VariantReplacementService $variantReplacementService,
|
private readonly VariantReplacementService $variantReplacementService,
|
||||||
private readonly InvalidateEventDateCartsService $invalidateEventDateCarts,
|
private readonly InvalidateEventDateCartsService $invalidateEventDateCarts,
|
||||||
|
private readonly EventAddressService $eventAddressService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public function forTenant(Tenant $tenant): Event
|
public function forTenant(Tenant $tenant): Event
|
||||||
{
|
{
|
||||||
return $tenant->activeEvent->load(['dates.validityTime', 'socialMedia']);
|
return $tenant->activeEvent->load(['dates.validityTime', 'socialMedia', 'address']);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @param array<string, mixed> $data */
|
/** @param array<string, mixed> $data */
|
||||||
@@ -49,6 +50,7 @@ class EventService
|
|||||||
'location' => $data['location'],
|
'location' => $data['location'],
|
||||||
...array_intersect_key($data, ['attachment_id' => true, 'exact_location' => true]),
|
...array_intersect_key($data, ['attachment_id' => true, 'exact_location' => true]),
|
||||||
]);
|
]);
|
||||||
|
$this->eventAddressService->syncLocation($event, array_key_exists('exact_location', $data));
|
||||||
$tenant->update([
|
$tenant->update([
|
||||||
...array_intersect_key($data, array_flip([
|
...array_intersect_key($data, array_flip([
|
||||||
'allow_ticket_refund',
|
'allow_ticket_refund',
|
||||||
@@ -64,7 +66,7 @@ class EventService
|
|||||||
$this->syncLegacyContact($event, $data['contact']);
|
$this->syncLegacyContact($event, $data['contact']);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $event->load(['dates.validityTime', 'socialMedia']);
|
return $event->load(['dates.validityTime', 'socialMedia', 'address']);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,11 +23,12 @@ class PublicEventService
|
|||||||
$query->where(function ($query) use ($term): void {
|
$query->where(function ($query) use ($term): void {
|
||||||
$query->where('title', 'like', '%'.$term.'%')
|
$query->where('title', 'like', '%'.$term.'%')
|
||||||
->orWhere('subtitle', 'like', '%'.$term.'%')
|
->orWhere('subtitle', 'like', '%'.$term.'%')
|
||||||
->orWhere('location', 'like', '%'.$term.'%');
|
->orWhereHas('address', fn ($query) => $query->where('address_text', 'like', '%'.$term.'%'))
|
||||||
|
->orWhere(fn ($query) => $query->whereNull('address_id')->where('location', 'like', '%'.$term.'%'));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return $query->with('attachment')->orderByDesc('published_at')->orderByDesc('id')->paginate(15, ['*'], 'page', $page);
|
return $query->with(['attachment', 'address'])->orderByDesc('published_at')->orderByDesc('id')->paginate(15, ['*'], 'page', $page);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function find(Tenant $tenant, Event $event): Event
|
public function find(Tenant $tenant, Event $event): Event
|
||||||
@@ -38,6 +39,7 @@ class PublicEventService
|
|||||||
|
|
||||||
return $event->load([
|
return $event->load([
|
||||||
'attachment',
|
'attachment',
|
||||||
|
'address',
|
||||||
'dates' => fn ($query) => $query->whereNull('rescheduled_to_event_date_id')->whereNull('suspended_at'),
|
'dates' => fn ($query) => $query->whereNull('rescheduled_to_event_date_id')->whereNull('suspended_at'),
|
||||||
'socialMedia',
|
'socialMedia',
|
||||||
'catalogItems.attachments',
|
'catalogItems.attachments',
|
||||||
|
|||||||
@@ -13,6 +13,32 @@ mediante `event_id`. El catálogo `social_media` define las plataformas;
|
|||||||
`exact_location` guarda opcionalmente un objeto JSON con `latitude` (-90 a 90)
|
`exact_location` guarda opcionalmente un objeto JSON con `latitude` (-90 a 90)
|
||||||
y `longitude` (-180 a 180). `location` sigue siendo la dirección legible.
|
y `longitude` (-180 a 180). `location` sigue siendo la dirección legible.
|
||||||
|
|
||||||
|
`events.address_id` es una referencia opcional a `addresses.id`: `Event::address()`
|
||||||
|
obtiene su dirección y `Address::events()` permite reutilizar una dirección en varios
|
||||||
|
eventos. Al borrar una dirección, el vínculo queda en `NULL` y se conserva el evento.
|
||||||
|
|
||||||
|
La migración `2026_09_30_000200_add_address_id_to_events` agrega el vínculo.
|
||||||
|
`2026_09_30_000300_copy_event_locations_to_addresses` copia `location` a
|
||||||
|
`address_text` y las coordenadas de `exact_location` a `latitude` y `longitude`, con
|
||||||
|
el label `Ubicación del evento`. Solo copia eventos sin `address_id` y con texto de
|
||||||
|
ubicación; no modifica los campos ni los timestamps originales, ni vincula la
|
||||||
|
dirección al tenant. Si faltan texto y coordenadas, el vínculo queda en `NULL`.
|
||||||
|
Los datos inválidos, incluidas coordenadas sin texto de dirección, detienen la
|
||||||
|
copia y revierten las inserciones para evitar una migración parcial. Las migraciones
|
||||||
|
deben ejecutarse explícitamente por quien administra la base.
|
||||||
|
|
||||||
|
Las respuestas públicas, administrativas y el evento del bootstrap incluyen
|
||||||
|
`address` con `id`, `label`, `address_text`, `latitude` y `longitude`. Las coordenadas
|
||||||
|
se serializan como números o `null`. Los campos anteriores de la respuesta conservan
|
||||||
|
sus valores originales; el frontend lee la ubicación y coordenadas desde `address`.
|
||||||
|
El guardado administrativo de
|
||||||
|
`location` y `exact_location` también actualiza `addresses`; si una dirección está
|
||||||
|
compartida con otro evento o con un tenant, se crea una copia para el evento editado.
|
||||||
|
El tenant devuelve `addresses` con `is_main` y `main_address`. El storefront usa
|
||||||
|
todas las direcciones en contacto, la principal en el footer y la propia del evento
|
||||||
|
en sus tarjetas, cabecera y mapa. Solo se agregan al mapa las direcciones con ambas
|
||||||
|
coordenadas.
|
||||||
|
|
||||||
La migración `2026_09_18_000500_restore_events` crea un evento por cada tenant que
|
La migración `2026_09_18_000500_restore_events` crea un evento por cada tenant que
|
||||||
tenía datos de evento o fechas, y migra las fechas y redes correspondientes.
|
tenía datos de evento o fechas, y migra las fechas y redes correspondientes.
|
||||||
`2026_09_18_000600_associate_active_event_social_media` copia al evento activo las
|
`2026_09_18_000600_associate_active_event_social_media` copia al evento activo las
|
||||||
|
|||||||
@@ -19,48 +19,48 @@ Route::prefix('v1/adminapp/tenant')
|
|||||||
->middleware("tenant.menu:{$menuCode}");
|
->middleware("tenant.menu:{$menuCode}");
|
||||||
};
|
};
|
||||||
|
|
||||||
$visibilityRoutes('entries', 'entries', 'adminapp.fiesta-futbol-infantil.entradas');
|
$visibilityRoutes('entries', 'entries', 'onticket.adminapp.fiesta-futbol-infantil.entradas');
|
||||||
$visibilityRoutes('foods', 'foods', 'adminapp.fiesta-futbol-infantil.comida');
|
$visibilityRoutes('foods', 'foods', 'onticket.adminapp.fiesta-futbol-infantil.comida');
|
||||||
$visibilityRoutes('accommodations', 'accommodations', 'adminapp.fiesta-futbol-infantil.alojamientos');
|
$visibilityRoutes('accommodations', 'accommodations', 'onticket.adminapp.fiesta-futbol-infantil.alojamientos');
|
||||||
$visibilityRoutes('merchandise', 'merchandise', 'adminapp.fiesta-futbol-infantil.merchandising');
|
$visibilityRoutes('merchandise', 'merchandise', 'onticket.adminapp.fiesta-futbol-infantil.merchandising');
|
||||||
|
|
||||||
Route::get('entries', [EntryController::class, 'index'])
|
Route::get('entries', [EntryController::class, 'index'])
|
||||||
->middleware('tenant.menu:adminapp.fiesta-futbol-infantil.entradas')
|
->middleware('tenant.menu:onticket.adminapp.fiesta-futbol-infantil.entradas')
|
||||||
->name('adminapp.fiesta-futbol-infantil.entries.index');
|
->name('adminapp.fiesta-futbol-infantil.entries.index');
|
||||||
Route::post('entries', [EntryController::class, 'store'])
|
Route::post('entries', [EntryController::class, 'store'])
|
||||||
->middleware('tenant.menu:adminapp.fiesta-futbol-infantil.entradas')
|
->middleware('tenant.menu:onticket.adminapp.fiesta-futbol-infantil.entradas')
|
||||||
->name('adminapp.fiesta-futbol-infantil.entries.store');
|
->name('adminapp.fiesta-futbol-infantil.entries.store');
|
||||||
Route::delete('entries/{entry}', [EntryController::class, 'destroy'])
|
Route::delete('entries/{entry}', [EntryController::class, 'destroy'])
|
||||||
->middleware('tenant.menu:adminapp.fiesta-futbol-infantil.entradas')
|
->middleware('tenant.menu:onticket.adminapp.fiesta-futbol-infantil.entradas')
|
||||||
->name('adminapp.fiesta-futbol-infantil.entries.destroy');
|
->name('adminapp.fiesta-futbol-infantil.entries.destroy');
|
||||||
Route::get('foods', [FoodController::class, 'index'])
|
Route::get('foods', [FoodController::class, 'index'])
|
||||||
->middleware('tenant.menu:adminapp.fiesta-futbol-infantil.comida')
|
->middleware('tenant.menu:onticket.adminapp.fiesta-futbol-infantil.comida')
|
||||||
->name('adminapp.fiesta-futbol-infantil.foods.index');
|
->name('adminapp.fiesta-futbol-infantil.foods.index');
|
||||||
Route::post('foods', [FoodController::class, 'store'])
|
Route::post('foods', [FoodController::class, 'store'])
|
||||||
->middleware('tenant.menu:adminapp.fiesta-futbol-infantil.comida')
|
->middleware('tenant.menu:onticket.adminapp.fiesta-futbol-infantil.comida')
|
||||||
->name('adminapp.fiesta-futbol-infantil.foods.store');
|
->name('adminapp.fiesta-futbol-infantil.foods.store');
|
||||||
Route::patch('foods/history-stock', [FoodController::class, 'updateHistoricalStock'])
|
Route::patch('foods/history-stock', [FoodController::class, 'updateHistoricalStock'])
|
||||||
->middleware('tenant.menu:adminapp.fiesta-futbol-infantil.comida')
|
->middleware('tenant.menu:onticket.adminapp.fiesta-futbol-infantil.comida')
|
||||||
->name('adminapp.fiesta-futbol-infantil.foods.history-stock.update');
|
->name('adminapp.fiesta-futbol-infantil.foods.history-stock.update');
|
||||||
Route::delete('foods/{food}', [FoodController::class, 'destroy'])
|
Route::delete('foods/{food}', [FoodController::class, 'destroy'])
|
||||||
->middleware('tenant.menu:adminapp.fiesta-futbol-infantil.comida')
|
->middleware('tenant.menu:onticket.adminapp.fiesta-futbol-infantil.comida')
|
||||||
->name('adminapp.fiesta-futbol-infantil.foods.destroy');
|
->name('adminapp.fiesta-futbol-infantil.foods.destroy');
|
||||||
Route::get('accommodations', [AccommodationController::class, 'index'])
|
Route::get('accommodations', [AccommodationController::class, 'index'])
|
||||||
->middleware('tenant.menu:adminapp.fiesta-futbol-infantil.alojamientos')
|
->middleware('tenant.menu:onticket.adminapp.fiesta-futbol-infantil.alojamientos')
|
||||||
->name('adminapp.fiesta-futbol-infantil.accommodations.index');
|
->name('adminapp.fiesta-futbol-infantil.accommodations.index');
|
||||||
Route::post('accommodations', [AccommodationController::class, 'store'])
|
Route::post('accommodations', [AccommodationController::class, 'store'])
|
||||||
->middleware('tenant.menu:adminapp.fiesta-futbol-infantil.alojamientos')
|
->middleware('tenant.menu:onticket.adminapp.fiesta-futbol-infantil.alojamientos')
|
||||||
->name('adminapp.fiesta-futbol-infantil.accommodations.store');
|
->name('adminapp.fiesta-futbol-infantil.accommodations.store');
|
||||||
Route::delete('accommodations/{accommodation}', [AccommodationController::class, 'destroy'])
|
Route::delete('accommodations/{accommodation}', [AccommodationController::class, 'destroy'])
|
||||||
->middleware('tenant.menu:adminapp.fiesta-futbol-infantil.alojamientos')
|
->middleware('tenant.menu:onticket.adminapp.fiesta-futbol-infantil.alojamientos')
|
||||||
->name('adminapp.fiesta-futbol-infantil.accommodations.destroy');
|
->name('adminapp.fiesta-futbol-infantil.accommodations.destroy');
|
||||||
Route::get('merchandise', [MerchandiseController::class, 'index'])
|
Route::get('merchandise', [MerchandiseController::class, 'index'])
|
||||||
->middleware('tenant.menu:adminapp.fiesta-futbol-infantil.merchandising')
|
->middleware('tenant.menu:onticket.adminapp.fiesta-futbol-infantil.merchandising')
|
||||||
->name('adminapp.fiesta-futbol-infantil.merchandise.index');
|
->name('adminapp.fiesta-futbol-infantil.merchandise.index');
|
||||||
Route::post('merchandise', [MerchandiseController::class, 'store'])
|
Route::post('merchandise', [MerchandiseController::class, 'store'])
|
||||||
->middleware('tenant.menu:adminapp.fiesta-futbol-infantil.merchandising')
|
->middleware('tenant.menu:onticket.adminapp.fiesta-futbol-infantil.merchandising')
|
||||||
->name('adminapp.fiesta-futbol-infantil.merchandise.store');
|
->name('adminapp.fiesta-futbol-infantil.merchandise.store');
|
||||||
Route::delete('merchandise/{merchandise}', [MerchandiseController::class, 'destroy'])
|
Route::delete('merchandise/{merchandise}', [MerchandiseController::class, 'destroy'])
|
||||||
->middleware('tenant.menu:adminapp.fiesta-futbol-infantil.merchandising')
|
->middleware('tenant.menu:onticket.adminapp.fiesta-futbol-infantil.merchandising')
|
||||||
->name('adminapp.fiesta-futbol-infantil.merchandise.destroy');
|
->name('adminapp.fiesta-futbol-infantil.merchandise.destroy');
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ Bajo `/tenants/{tenant:codigo}`, protegidos por `auth:sanctum`:
|
|||||||
- `POST /tickets/pdf`.
|
- `POST /tickets/pdf`.
|
||||||
|
|
||||||
Bajo `/v1/adminapp/tenant`, protegido por `auth:sanctum`, `adminapp.tenant` y el menú
|
Bajo `/v1/adminapp/tenant`, protegido por `auth:sanctum`, `adminapp.tenant` y el menú
|
||||||
`adminapp.tickets`:
|
`onticket.adminapp.tickets`:
|
||||||
|
|
||||||
- `GET /tickets`, paginado y con búsqueda opcional mediante `q`. La respuesta incluye
|
- `GET /tickets`, paginado y con búsqueda opcional mediante `q`. La respuesta incluye
|
||||||
`scanned_tickets` y `total_tickets` para el tenant autenticado.
|
`scanned_tickets` y `total_tickets` para el tenant autenticado.
|
||||||
|
|||||||
@@ -7,24 +7,24 @@ Route::prefix('v1/adminapp/tenant')
|
|||||||
->middleware(['auth:sanctum', 'adminapp.tenant'])
|
->middleware(['auth:sanctum', 'adminapp.tenant'])
|
||||||
->group(function (): void {
|
->group(function (): void {
|
||||||
Route::get('tickets', [TicketController::class, 'index'])
|
Route::get('tickets', [TicketController::class, 'index'])
|
||||||
->middleware('tenant.menu:adminapp.tickets')
|
->middleware('tenant.menu:onticket.adminapp.tickets')
|
||||||
->name('adminapp.tickets.index');
|
->name('adminapp.tickets.index');
|
||||||
Route::post('tickets/{ticket}/cancel', [TicketController::class, 'cancel'])
|
Route::post('tickets/{ticket}/cancel', [TicketController::class, 'cancel'])
|
||||||
->whereNumber('ticket')
|
->whereNumber('ticket')
|
||||||
->middleware('tenant.menu:adminapp.tickets')
|
->middleware('tenant.menu:onticket.adminapp.tickets')
|
||||||
->name('adminapp.tickets.cancel');
|
->name('adminapp.tickets.cancel');
|
||||||
Route::get('tickets/{ticket}/refund', [TicketController::class, 'calculateRefund'])
|
Route::get('tickets/{ticket}/refund', [TicketController::class, 'calculateRefund'])
|
||||||
->whereNumber('ticket')
|
->whereNumber('ticket')
|
||||||
->middleware('tenant.menu:adminapp.tickets')
|
->middleware('tenant.menu:onticket.adminapp.tickets')
|
||||||
->name('adminapp.tickets.calculate-refund');
|
->name('adminapp.tickets.calculate-refund');
|
||||||
Route::post('tickets/{ticket}/refund', [TicketController::class, 'refund'])
|
Route::post('tickets/{ticket}/refund', [TicketController::class, 'refund'])
|
||||||
->whereNumber('ticket')
|
->whereNumber('ticket')
|
||||||
->middleware('tenant.menu:adminapp.tickets')
|
->middleware('tenant.menu:onticket.adminapp.tickets')
|
||||||
->name('adminapp.tickets.refund');
|
->name('adminapp.tickets.refund');
|
||||||
Route::get('tickets/pdf', [TicketController::class, 'downloadPdf'])
|
Route::get('tickets/pdf', [TicketController::class, 'downloadPdf'])
|
||||||
->middleware('tenant.menu:adminapp.tickets')
|
->middleware('tenant.menu:onticket.adminapp.tickets')
|
||||||
->name('adminapp.tickets.pdf');
|
->name('adminapp.tickets.pdf');
|
||||||
Route::get('tickets/excel', [TicketController::class, 'downloadExcel'])
|
Route::get('tickets/excel', [TicketController::class, 'downloadExcel'])
|
||||||
->middleware('tenant.menu:adminapp.tickets')
|
->middleware('tenant.menu:onticket.adminapp.tickets')
|
||||||
->name('adminapp.tickets.excel');
|
->name('adminapp.tickets.excel');
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -13,6 +13,9 @@ class EnsureTenantHasMenu
|
|||||||
$tenant = $request->user()?->tenant()->first();
|
$tenant = $request->user()?->tenant()->first();
|
||||||
$hasMenu = $tenant?->menues()
|
$hasMenu = $tenant?->menues()
|
||||||
->where('menues.code', $menuCode)
|
->where('menues.code', $menuCode)
|
||||||
|
->where(fn ($query) => $query
|
||||||
|
->where('menues.code', 'help.contact')
|
||||||
|
->orWhere('tenants_menues.is_enabled', true))
|
||||||
->exists() ?? false;
|
->exists() ?? false;
|
||||||
|
|
||||||
abort_unless($hasMenu, 404);
|
abort_unless($hasMenu, 404);
|
||||||
|
|||||||
@@ -18,12 +18,12 @@ Route::prefix('v1/adminapp/forms')
|
|||||||
Route::get(
|
Route::get(
|
||||||
'desfile/entry-reservation',
|
'desfile/entry-reservation',
|
||||||
DesfileEntryReservationFormController::class
|
DesfileEntryReservationFormController::class
|
||||||
)->middleware('tenant.menu:adminapp.desfile.reservas')
|
)->middleware('tenant.menu:onticket.adminapp.desfile.reservas')
|
||||||
->name('adminapp.forms.desfile.entry-reservation');
|
->name('adminapp.forms.desfile.entry-reservation');
|
||||||
Route::get('sale', SaleFormController::class);
|
Route::get('sale', SaleFormController::class);
|
||||||
Route::get('staff', StaffFormController::class);
|
Route::get('staff', StaffFormController::class);
|
||||||
Route::get('tickets-filter', TicketFilterFormController::class)
|
Route::get('tickets-filter', TicketFilterFormController::class)
|
||||||
->middleware('tenant.menu:adminapp.tickets')
|
->middleware('tenant.menu:onticket.adminapp.tickets')
|
||||||
->name('adminapp.forms.tickets-filter');
|
->name('adminapp.forms.tickets-filter');
|
||||||
Route::get(
|
Route::get(
|
||||||
'fiesta-futbol-infantil/ticket',
|
'fiesta-futbol-infantil/ticket',
|
||||||
|
|||||||
@@ -3,11 +3,12 @@
|
|||||||
namespace App\Shared\Integration\Services;
|
namespace App\Shared\Integration\Services;
|
||||||
|
|
||||||
use App\Domains\Core\Client\Models\Client;
|
use App\Domains\Core\Client\Models\Client;
|
||||||
|
use App\Domains\Core\Tenant\Models\Tenant;
|
||||||
|
use App\Domains\Ticketing\Event\Models\Event;
|
||||||
|
use App\Shared\Integration\Models\AdminWebsiteTypeIntegration;
|
||||||
use App\Shared\Integration\Models\ClientIntegration;
|
use App\Shared\Integration\Models\ClientIntegration;
|
||||||
use App\Shared\Integration\Models\Integration;
|
use App\Shared\Integration\Models\Integration;
|
||||||
use App\Shared\Integration\Models\IntegrationInstance;
|
use App\Shared\Integration\Models\IntegrationInstance;
|
||||||
use App\Shared\Integration\Models\AdminWebsiteTypeIntegration;
|
|
||||||
use App\Domains\Core\Tenant\Models\Tenant;
|
|
||||||
use Exception;
|
use Exception;
|
||||||
use Illuminate\Http\Client\PendingRequest;
|
use Illuminate\Http\Client\PendingRequest;
|
||||||
use Illuminate\Support\Facades\Http;
|
use Illuminate\Support\Facades\Http;
|
||||||
@@ -86,6 +87,17 @@ abstract class BaseIntegrationService
|
|||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function forEvent(Event $event): self
|
||||||
|
{
|
||||||
|
$event->loadMissing(['client', 'tenant.client']);
|
||||||
|
$this->tenant = $event->tenant;
|
||||||
|
$this->tenantCode = $event->tenant_code;
|
||||||
|
$this->clientContext = $event->effectiveClient();
|
||||||
|
$this->loadIntegration();
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load the integration definition and its effective instance configuration.
|
* Load the integration definition and its effective instance configuration.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -3,8 +3,9 @@
|
|||||||
namespace App\Shared\Integration\Services;
|
namespace App\Shared\Integration\Services;
|
||||||
|
|
||||||
use App\Domains\Core\Client\Models\Client;
|
use App\Domains\Core\Client\Models\Client;
|
||||||
use App\Domains\Core\Tenant\Models\Tenant;
|
|
||||||
use App\Domains\Core\Tenant\Models\AdminWebsiteType;
|
use App\Domains\Core\Tenant\Models\AdminWebsiteType;
|
||||||
|
use App\Domains\Core\Tenant\Models\Tenant;
|
||||||
|
use App\Domains\Ticketing\Event\Models\Event;
|
||||||
use Exception;
|
use Exception;
|
||||||
use Illuminate\Contracts\Mail\Factory as MailFactory;
|
use Illuminate\Contracts\Mail\Factory as MailFactory;
|
||||||
use Illuminate\Contracts\Mail\Mailer;
|
use Illuminate\Contracts\Mail\Mailer;
|
||||||
@@ -39,14 +40,7 @@ class MailService extends BaseIntegrationService
|
|||||||
public function forTenant(string $tenantCode): self
|
public function forTenant(string $tenantCode): self
|
||||||
{
|
{
|
||||||
parent::forTenant($tenantCode);
|
parent::forTenant($tenantCode);
|
||||||
|
$this->configureMailer();
|
||||||
if ($this->integrationInstance) {
|
|
||||||
$this->mailer = $this->resolveMailer();
|
|
||||||
$this->usesInstanceMailer = true;
|
|
||||||
} else {
|
|
||||||
$this->mailer = $this->mailFactory->mailer();
|
|
||||||
$this->usesInstanceMailer = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
@@ -55,16 +49,30 @@ class MailService extends BaseIntegrationService
|
|||||||
{
|
{
|
||||||
parent::forClient($client);
|
parent::forClient($client);
|
||||||
$this->tenant = $this->clientContext?->tenants()->first();
|
$this->tenant = $this->clientContext?->tenants()->first();
|
||||||
|
$this->configureMailer();
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function forEvent(Event $event): self
|
||||||
|
{
|
||||||
|
parent::forEvent($event);
|
||||||
|
$this->configureMailer();
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function configureMailer(): void
|
||||||
|
{
|
||||||
if ($this->integrationInstance) {
|
if ($this->integrationInstance) {
|
||||||
$this->mailer = $this->resolveMailer();
|
$this->mailer = $this->resolveMailer();
|
||||||
$this->usesInstanceMailer = true;
|
$this->usesInstanceMailer = true;
|
||||||
} else {
|
|
||||||
$this->mailer = $this->mailFactory->mailer();
|
return;
|
||||||
$this->usesInstanceMailer = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this;
|
$this->mailer = $this->mailFactory->mailer();
|
||||||
|
$this->usesInstanceMailer = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getHeaders(): array
|
public function getHeaders(): array
|
||||||
@@ -83,7 +91,7 @@ class MailService extends BaseIntegrationService
|
|||||||
array $attachments = [],
|
array $attachments = [],
|
||||||
): void {
|
): void {
|
||||||
if (! $this->mailer || ! $this->tenant) {
|
if (! $this->mailer || ! $this->tenant) {
|
||||||
throw new Exception('MailService no está configurado. Llamá a forTenant() o forClient() primero.');
|
throw new Exception('MailService no está configurado. Llamá a forTenant(), forClient() o forEvent() primero.');
|
||||||
}
|
}
|
||||||
|
|
||||||
$brand ??= $this->tenant;
|
$brand ??= $this->tenant;
|
||||||
@@ -160,7 +168,7 @@ class MailService extends BaseIntegrationService
|
|||||||
public function onSetup(): void
|
public function onSetup(): void
|
||||||
{
|
{
|
||||||
if (! $this->mailer || ! $this->clientContext) {
|
if (! $this->mailer || ! $this->clientContext) {
|
||||||
throw new Exception('MailService no está configurado. Llamá a forTenant() o forClient() primero.');
|
throw new Exception('MailService no está configurado. Llamá a forTenant(), forClient() o forEvent() primero.');
|
||||||
}
|
}
|
||||||
|
|
||||||
$recipient = $this->getIntegrationSetting('MAIL_FROM_ADDRESS');
|
$recipient = $this->getIntegrationSetting('MAIL_FROM_ADDRESS');
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ class TelepagosIntegrationService extends BaseIntegrationService
|
|||||||
public function getToken(): string
|
public function getToken(): string
|
||||||
{
|
{
|
||||||
if (! $this->integrationInstance) {
|
if (! $this->integrationInstance) {
|
||||||
throw new Exception('Client integration is not loaded. Call forTenant() or forClient() first.');
|
throw new Exception('Client integration is not loaded. Call forTenant(), forClient(), or forEvent() first.');
|
||||||
}
|
}
|
||||||
|
|
||||||
$cacheKey = $this->integrationInstance->tokenCacheKey();
|
$cacheKey = $this->integrationInstance->tokenCacheKey();
|
||||||
|
|||||||
@@ -2,12 +2,12 @@
|
|||||||
|
|
||||||
namespace App\Shared\Integration\Services;
|
namespace App\Shared\Integration\Services;
|
||||||
|
|
||||||
use App\Domains\Core\Client\Models\Client;
|
|
||||||
use App\Domains\Commerce\Purchase\Models\Purchase;
|
use App\Domains\Commerce\Purchase\Models\Purchase;
|
||||||
use App\Domains\Commerce\Purchase\Models\TelepagosPayment;
|
use App\Domains\Commerce\Purchase\Models\TelepagosPayment;
|
||||||
use App\Domains\Commerce\Purchase\Models\TelepagosQr;
|
use App\Domains\Commerce\Purchase\Models\TelepagosQr;
|
||||||
use App\Domains\Commerce\Purchase\Services\CheckoutService;
|
use App\Domains\Commerce\Purchase\Services\CheckoutService;
|
||||||
use App\Domains\Commerce\Purchase\Services\DniDistanceService;
|
use App\Domains\Commerce\Purchase\Services\DniDistanceService;
|
||||||
|
use App\Domains\Core\Client\Models\Client;
|
||||||
use Exception;
|
use Exception;
|
||||||
use Illuminate\Database\Eloquent\Builder;
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
use Illuminate\Database\Eloquent\Collection;
|
use Illuminate\Database\Eloquent\Collection;
|
||||||
@@ -75,9 +75,30 @@ class TelepagosWebhookService
|
|||||||
|
|
||||||
$dni = substr($cuit, 2, -1);
|
$dni = substr($cuit, 2, -1);
|
||||||
|
|
||||||
$tenantCodes = $client->tenants()->pluck('codigo');
|
|
||||||
$eligiblePurchases = Purchase::query()
|
$eligiblePurchases = Purchase::query()
|
||||||
->whereIn('tenant_codigo', $tenantCodes)
|
->where(function (Builder $purchases) use ($client): void {
|
||||||
|
$purchases
|
||||||
|
->whereHas('event', function (Builder $events) use ($client): void {
|
||||||
|
$events
|
||||||
|
->where('client_id', $client->id)
|
||||||
|
->orWhere(function (Builder $fallback) use ($client): void {
|
||||||
|
$fallback
|
||||||
|
->whereNull('client_id')
|
||||||
|
->whereHas(
|
||||||
|
'tenant',
|
||||||
|
fn (Builder $tenants): Builder => $tenants->where('client_id', $client->id),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
->orWhere(function (Builder $legacy) use ($client): void {
|
||||||
|
$legacy
|
||||||
|
->whereNull('event_id')
|
||||||
|
->whereHas(
|
||||||
|
'tenant',
|
||||||
|
fn (Builder $tenants): Builder => $tenants->where('client_id', $client->id),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
})
|
||||||
->whereIn('status', [
|
->whereIn('status', [
|
||||||
Purchase::STATUS_CREATED,
|
Purchase::STATUS_CREATED,
|
||||||
Purchase::STATUS_PENDING_PAYMENT,
|
Purchase::STATUS_PENDING_PAYMENT,
|
||||||
@@ -170,7 +191,7 @@ class TelepagosWebhookService
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (! $client->tenants()->where('codigo', $compra->tenant_codigo)->exists()) {
|
if (! $this->purchaseBelongsToClient($compra, $client)) {
|
||||||
Log::channel('telepagos')->warning('Telepagos webhook: Purchase does not belong to client.', [
|
Log::channel('telepagos')->warning('Telepagos webhook: Purchase does not belong to client.', [
|
||||||
'client_code' => $client->code,
|
'client_code' => $client->code,
|
||||||
'cashin_id' => $cashinId,
|
'cashin_id' => $cashinId,
|
||||||
@@ -244,6 +265,16 @@ class TelepagosWebhookService
|
|||||||
return number_format((float) $amount, 2, '.', '');
|
return number_format((float) $amount, 2, '.', '');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function purchaseBelongsToClient(Purchase $purchase, Client $client): bool
|
||||||
|
{
|
||||||
|
$purchase->loadMissing(['event.client', 'event.tenant.client', 'tenant.client']);
|
||||||
|
|
||||||
|
$effectiveClient = $purchase->event?->effectiveClient()
|
||||||
|
?? $purchase->tenant?->client;
|
||||||
|
|
||||||
|
return $effectiveClient?->is($client) ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param Builder<Purchase> $eligiblePurchases
|
* @param Builder<Purchase> $eligiblePurchases
|
||||||
* @return Collection<int, Purchase>
|
* @return Collection<int, Purchase>
|
||||||
|
|||||||
@@ -2,16 +2,16 @@
|
|||||||
|
|
||||||
namespace App\Shared\Notification\Services;
|
namespace App\Shared\Notification\Services;
|
||||||
|
|
||||||
|
use App\Domains\Commerce\Purchase\Models\Purchase;
|
||||||
use App\Domains\Core\Auth\Models\ResetPasswordAttempt;
|
use App\Domains\Core\Auth\Models\ResetPasswordAttempt;
|
||||||
use App\Domains\Core\Auth\Models\User;
|
use App\Domains\Core\Auth\Models\User;
|
||||||
use App\Shared\Integration\Services\MailService;
|
|
||||||
use App\Shared\Notification\Events\PasswordResetRequested;
|
|
||||||
use App\Domains\Commerce\Purchase\Models\Purchase;
|
|
||||||
use App\Domains\Core\Tenant\Models\Tenant;
|
use App\Domains\Core\Tenant\Models\Tenant;
|
||||||
use App\Domains\Ticketing\Ticket\Models\Ticket;
|
use App\Domains\Ticketing\Ticket\Models\Ticket;
|
||||||
use App\Domains\Ticketing\Ticket\Services\TicketPdfService;
|
use App\Domains\Ticketing\Ticket\Services\TicketPdfService;
|
||||||
use App\Domains\Ticketing\Ticket\Services\TicketPresentationResolver;
|
use App\Domains\Ticketing\Ticket\Services\TicketPresentationResolver;
|
||||||
use App\Domains\Ticketing\Ticket\Services\TicketValidityResolver;
|
use App\Domains\Ticketing\Ticket\Services\TicketValidityResolver;
|
||||||
|
use App\Shared\Integration\Services\MailService;
|
||||||
|
use App\Shared\Notification\Events\PasswordResetRequested;
|
||||||
use Closure;
|
use Closure;
|
||||||
use Illuminate\Support\Collection;
|
use Illuminate\Support\Collection;
|
||||||
use Illuminate\Support\Facades\Log;
|
use Illuminate\Support\Facades\Log;
|
||||||
@@ -158,7 +158,7 @@ class NotificationMailService
|
|||||||
$context = ['purchase_id' => $purchaseId];
|
$context = ['purchase_id' => $purchaseId];
|
||||||
|
|
||||||
$purchase = Purchase::query()
|
$purchase = Purchase::query()
|
||||||
->with(['tenant', 'user', 'items'])
|
->with(['event', 'tenant', 'user', 'items'])
|
||||||
->find($purchaseId);
|
->find($purchaseId);
|
||||||
|
|
||||||
if ($purchase === null) {
|
if ($purchase === null) {
|
||||||
@@ -197,8 +197,7 @@ class NotificationMailService
|
|||||||
'mime' => 'application/pdf',
|
'mime' => 'application/pdf',
|
||||||
]];
|
]];
|
||||||
|
|
||||||
$this->mailService
|
$this->mailForPurchase($purchase)
|
||||||
->forTenant($purchase->tenant_codigo)
|
|
||||||
->send(
|
->send(
|
||||||
$recipient,
|
$recipient,
|
||||||
"Compra confirmada - Compra #{$purchase->getKey()}",
|
"Compra confirmada - Compra #{$purchase->getKey()}",
|
||||||
@@ -286,7 +285,6 @@ class NotificationMailService
|
|||||||
$context,
|
$context,
|
||||||
$recipient,
|
$recipient,
|
||||||
function () use (
|
function () use (
|
||||||
$tenantCode,
|
|
||||||
$purchase,
|
$purchase,
|
||||||
$recipient,
|
$recipient,
|
||||||
$previousDate,
|
$previousDate,
|
||||||
@@ -295,8 +293,7 @@ class NotificationMailService
|
|||||||
): array {
|
): array {
|
||||||
$brand = $purchase->tenant;
|
$brand = $purchase->tenant;
|
||||||
|
|
||||||
$this->mailService
|
$this->mailForPurchase($purchase)
|
||||||
->forTenant($tenantCode)
|
|
||||||
->send(
|
->send(
|
||||||
$recipient,
|
$recipient,
|
||||||
"Tu evento fue reprogramado - N° de Orden #{$purchase->getKey()}",
|
"Tu evento fue reprogramado - N° de Orden #{$purchase->getKey()}",
|
||||||
@@ -387,7 +384,6 @@ class NotificationMailService
|
|||||||
$context,
|
$context,
|
||||||
$recipient,
|
$recipient,
|
||||||
function () use (
|
function () use (
|
||||||
$tenantCode,
|
|
||||||
$purchase,
|
$purchase,
|
||||||
$recipient,
|
$recipient,
|
||||||
$date,
|
$date,
|
||||||
@@ -396,8 +392,7 @@ class NotificationMailService
|
|||||||
): array {
|
): array {
|
||||||
$brand = $purchase->tenant;
|
$brand = $purchase->tenant;
|
||||||
|
|
||||||
$this->mailService
|
$this->mailForPurchase($purchase)
|
||||||
->forTenant($tenantCode)
|
|
||||||
->send(
|
->send(
|
||||||
$recipient,
|
$recipient,
|
||||||
"Una fecha de tu evento fue suspendida - N° de Orden #{$purchase->getKey()}",
|
"Una fecha de tu evento fue suspendida - N° de Orden #{$purchase->getKey()}",
|
||||||
@@ -421,10 +416,17 @@ class NotificationMailService
|
|||||||
{
|
{
|
||||||
return Purchase::query()
|
return Purchase::query()
|
||||||
->where('tenant_codigo', $tenantCode)
|
->where('tenant_codigo', $tenantCode)
|
||||||
->with(['tenant', 'user'])
|
->with(['event', 'tenant', 'user'])
|
||||||
->find($purchaseId);
|
->find($purchaseId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function mailForPurchase(Purchase $purchase): MailService
|
||||||
|
{
|
||||||
|
return $purchase->event
|
||||||
|
? $this->mailService->forEvent($purchase->event)
|
||||||
|
: $this->mailService->forTenant($purchase->tenant_codigo);
|
||||||
|
}
|
||||||
|
|
||||||
private function recipientFor(Purchase $purchase): string
|
private function recipientFor(Purchase $purchase): string
|
||||||
{
|
{
|
||||||
return (string) ($purchase->email ?: $purchase->user?->email);
|
return (string) ($purchase->email ?: $purchase->user?->email);
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ return new class extends Migration
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$sourcePath = public_path('images/website_types/'.self::FILENAME);
|
$sourcePath = public_path('images/website_types/onticket/'.self::FILENAME);
|
||||||
|
|
||||||
if (! is_file($sourcePath)) {
|
if (! is_file($sourcePath)) {
|
||||||
throw new RuntimeException("Favicon not found at path: {$sourcePath}");
|
throw new RuntimeException("Favicon not found at path: {$sourcePath}");
|
||||||
|
|||||||
@@ -52,8 +52,38 @@ return new class extends Migration
|
|||||||
|
|
||||||
private function assertNonNegative(string $expression, string $reason): void
|
private function assertNonNegative(string $expression, string $reason): void
|
||||||
{
|
{
|
||||||
if (DB::table('inventories')->whereRaw("({$expression}) < 0")->exists()) {
|
$invalidInventories = DB::table('inventories')
|
||||||
throw new RuntimeException("Hay inventarios inconsistentes: {$reason}.");
|
->select([
|
||||||
|
'id',
|
||||||
|
'real_stock',
|
||||||
|
'sold_units',
|
||||||
|
'refunded_units',
|
||||||
|
'reserved_stock',
|
||||||
|
'entry_reserved_stock',
|
||||||
|
])
|
||||||
|
->selectRaw("({$expression}) as calculated_stock")
|
||||||
|
->whereRaw("({$expression}) < 0")
|
||||||
|
->orderBy('id')
|
||||||
|
->limit(20)
|
||||||
|
->get();
|
||||||
|
|
||||||
|
if ($invalidInventories->isNotEmpty()) {
|
||||||
|
$details = $invalidInventories
|
||||||
|
->map(static fn (object $inventory): string => sprintf(
|
||||||
|
'id=%d [real=%d, vendidas=%d, reintegradas=%d, reservadas=%d, reservas_entradas=%d, resultado=%d]',
|
||||||
|
$inventory->id,
|
||||||
|
$inventory->real_stock,
|
||||||
|
$inventory->sold_units,
|
||||||
|
$inventory->refunded_units,
|
||||||
|
$inventory->reserved_stock,
|
||||||
|
$inventory->entry_reserved_stock,
|
||||||
|
$inventory->calculated_stock,
|
||||||
|
))
|
||||||
|
->implode('; ');
|
||||||
|
|
||||||
|
throw new RuntimeException(
|
||||||
|
"Hay inventarios inconsistentes: {$reason}. Inventarios detectados (máximo 20): {$details}."
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('events', function (Blueprint $table): void {
|
||||||
|
$table->foreignId('client_id')
|
||||||
|
->nullable()
|
||||||
|
->after('id')
|
||||||
|
->constrained('clients')
|
||||||
|
->restrictOnDelete();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('events', function (Blueprint $table): void {
|
||||||
|
$table->dropConstrainedForeignId('client_id');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Domains\Core\Tenant\Models\AdminWebsiteType;
|
||||||
|
use App\Domains\Core\Tenant\Services\AdminWebsiteTypeService;
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Http\UploadedFile;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
if (app()->environment('testing')) {
|
||||||
|
Storage::fake('s3');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! AdminWebsiteType::query()->where('codigo', 'shopit')->exists()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
app(AdminWebsiteTypeService::class)->updateOrCreate(
|
||||||
|
['codigo' => 'shopit'],
|
||||||
|
[
|
||||||
|
'primary_color' => '#2FD3AC',
|
||||||
|
'site_logo' => $this->uploadedImage('shopit_logo.png', 'image/png'),
|
||||||
|
'footer_logo' => $this->uploadedImage('shopit_footer_logo.png', 'image/png'),
|
||||||
|
'favicon' => $this->uploadedImage('shopit-favicon.svg', 'image/svg+xml'),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
// Brand assets are operational data and are intentionally preserved.
|
||||||
|
}
|
||||||
|
|
||||||
|
private function uploadedImage(string $filename, string $mimeType): UploadedFile
|
||||||
|
{
|
||||||
|
$path = public_path("images/website_types/shopit/{$filename}");
|
||||||
|
|
||||||
|
if (! is_file($path)) {
|
||||||
|
throw new RuntimeException("ShopIt image not found at path: {$path}");
|
||||||
|
}
|
||||||
|
|
||||||
|
return new UploadedFile($path, $filename, $mimeType, null, true);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('compras', function (Blueprint $table): void {
|
||||||
|
$table->foreignId('event_id')
|
||||||
|
->nullable()
|
||||||
|
->after('tenant_codigo')
|
||||||
|
->constrained('events')
|
||||||
|
->nullOnDelete();
|
||||||
|
});
|
||||||
|
|
||||||
|
DB::table('compras')
|
||||||
|
->orderBy('id')
|
||||||
|
->each(function (object $purchase): void {
|
||||||
|
$eventIds = DB::table('compra_items')
|
||||||
|
->join('catalog_items', 'catalog_items.id', '=', 'compra_items.source_catalog_item_id')
|
||||||
|
->where('compra_items.compra_id', $purchase->id)
|
||||||
|
->whereNotNull('catalog_items.event_id')
|
||||||
|
->distinct()
|
||||||
|
->pluck('catalog_items.event_id');
|
||||||
|
|
||||||
|
if ($eventIds->count() === 1) {
|
||||||
|
DB::table('compras')
|
||||||
|
->where('id', $purchase->id)
|
||||||
|
->update(['event_id' => $eventIds->first()]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('compras', function (Blueprint $table): void {
|
||||||
|
$table->dropConstrainedForeignId('event_id');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
private const EVENT_TITLE = '26.º Fiesta de la Tradición y 4.º Encuentro de Agrupaciones Gauchas';
|
||||||
|
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
$clientId = DB::table('clients')
|
||||||
|
->where('code', 'pyme_rural')
|
||||||
|
->value('id');
|
||||||
|
|
||||||
|
if ($clientId === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
DB::table('events')
|
||||||
|
->where('tenant_code', 'onticket')
|
||||||
|
->where('title', self::EVENT_TITLE)
|
||||||
|
->update(['client_id' => $clientId]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
$clientId = DB::table('clients')
|
||||||
|
->where('code', 'pyme_rural')
|
||||||
|
->value('id');
|
||||||
|
|
||||||
|
if ($clientId === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
DB::table('events')
|
||||||
|
->where('tenant_code', 'onticket')
|
||||||
|
->where('title', self::EVENT_TITLE)
|
||||||
|
->where('client_id', $clientId)
|
||||||
|
->update(['client_id' => null]);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,236 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
private const ONTICKET_MENUS = [
|
||||||
|
'adminapp.event' => ['onticket.adminapp.event', 'Eventos', '/admin/event'],
|
||||||
|
'adminapp.inicio' => ['onticket.adminapp.inicio', 'Inicio', '/admin/inicio'],
|
||||||
|
'adminapp.catalog' => ['onticket.adminapp.catalog', 'Catálogo', '/admin/catalog'],
|
||||||
|
'adminapp.combos' => ['onticket.adminapp.combos', 'Combos', '/admin/combos'],
|
||||||
|
'adminapp.categories' => ['onticket.adminapp.categories', 'Categorías', '/admin/categories'],
|
||||||
|
'adminapp.ventas' => ['onticket.adminapp.ventas', 'Ventas', '/admin/ventas'],
|
||||||
|
'adminapp.staff' => ['onticket.adminapp.staff', 'Usuarios', '/admin/staff'],
|
||||||
|
'adminapp.tickets' => ['onticket.adminapp.tickets', 'Tickets', '/admin/tickets'],
|
||||||
|
'adminapp.fiesta-futbol-infantil.entradas' => [
|
||||||
|
'onticket.adminapp.fiesta-futbol-infantil.entradas',
|
||||||
|
'Entradas',
|
||||||
|
'/admin/entradas',
|
||||||
|
],
|
||||||
|
'adminapp.fiesta-futbol-infantil.alojamientos' => [
|
||||||
|
'onticket.adminapp.fiesta-futbol-infantil.alojamientos',
|
||||||
|
'Alojamientos',
|
||||||
|
'/admin/alojamientos',
|
||||||
|
],
|
||||||
|
'adminapp.fiesta-futbol-infantil.merchandising' => [
|
||||||
|
'onticket.adminapp.fiesta-futbol-infantil.merchandising',
|
||||||
|
'Merchandising',
|
||||||
|
'/admin/merchandising',
|
||||||
|
],
|
||||||
|
'adminapp.fiesta-futbol-infantil.comida' => [
|
||||||
|
'onticket.adminapp.fiesta-futbol-infantil.comida',
|
||||||
|
'Comida',
|
||||||
|
'/admin/comidas',
|
||||||
|
],
|
||||||
|
'adminapp.desfile.entradas' => [
|
||||||
|
'onticket.adminapp.desfile.entradas',
|
||||||
|
'Entradas',
|
||||||
|
'/admin/desfile/entradas',
|
||||||
|
],
|
||||||
|
'adminapp.desfile.reservas' => [
|
||||||
|
'onticket.adminapp.desfile.reservas',
|
||||||
|
'Reserva de Tickets',
|
||||||
|
'/admin/desfile/reservas',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
private const SHOPIT_MENUS = [
|
||||||
|
'shopit.adminapp.marca' => ['Marca', '/admin/marca'],
|
||||||
|
'shopit.adminapp.contacto' => ['Contacto', '/admin/contacto'],
|
||||||
|
'shopit.adminapp.productos' => ['Productos', '/admin/productos'],
|
||||||
|
'shopit.adminapp.categorias' => ['Categorías', '/admin/categorias'],
|
||||||
|
'shopit.adminapp.transacciones' => ['Transacciones', '/admin/transacciones'],
|
||||||
|
'shopit.adminapp.reportes' => ['Reportes', '/admin/reportes'],
|
||||||
|
'shopit.adminapp.usuarios_roles' => ['Usuarios y roles', '/admin/usuarios-roles'],
|
||||||
|
];
|
||||||
|
|
||||||
|
private const LEGACY_SHOPIT_ADMIN_MENUS = [
|
||||||
|
'adminapp.event',
|
||||||
|
'adminapp.inicio',
|
||||||
|
'adminapp.catalog',
|
||||||
|
'adminapp.combos',
|
||||||
|
'adminapp.categories',
|
||||||
|
'adminapp.ventas',
|
||||||
|
'adminapp.staff',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
DB::transaction(function (): void {
|
||||||
|
if (! DB::table('menues')->where('code', 'main.adminapp')->exists()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (self::ONTICKET_MENUS as $legacyCode => [$code, $label, $route]) {
|
||||||
|
$this->replaceMenuCode($legacyCode, $code, $label, $route);
|
||||||
|
$this->assignMenuToAdminRoles($code);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (self::SHOPIT_MENUS as $code => [$label, $route]) {
|
||||||
|
$this->upsertMenu($code, $label, $route);
|
||||||
|
$this->assignMenuToAdminRoles($code);
|
||||||
|
}
|
||||||
|
|
||||||
|
$shopItTenantCodes = DB::table('tenants')
|
||||||
|
->where('admin_website_type_code', 'shopit')
|
||||||
|
->pluck('codigo');
|
||||||
|
|
||||||
|
if ($shopItTenantCodes->isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$adminMenuCodes = DB::table('menues')
|
||||||
|
->where('parent_menu_code', 'main.adminapp')
|
||||||
|
->pluck('code');
|
||||||
|
|
||||||
|
DB::table('tenants_menues')
|
||||||
|
->whereIn('tenant_code', $shopItTenantCodes)
|
||||||
|
->whereIn('menu_code', $adminMenuCodes)
|
||||||
|
->delete();
|
||||||
|
|
||||||
|
foreach ($shopItTenantCodes as $tenantCode) {
|
||||||
|
$this->assignMenuToTenant($tenantCode, 'main.adminapp');
|
||||||
|
|
||||||
|
foreach (array_keys(self::SHOPIT_MENUS) as $menuCode) {
|
||||||
|
$this->assignMenuToTenant($tenantCode, $menuCode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
DB::transaction(function (): void {
|
||||||
|
if (! DB::table('menues')->where('code', 'main.adminapp')->exists()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$shopItTenantCodes = DB::table('tenants')
|
||||||
|
->where('admin_website_type_code', 'shopit')
|
||||||
|
->pluck('codigo');
|
||||||
|
|
||||||
|
DB::table('tenants_menues')
|
||||||
|
->whereIn('tenant_code', $shopItTenantCodes)
|
||||||
|
->whereIn('menu_code', array_keys(self::SHOPIT_MENUS))
|
||||||
|
->delete();
|
||||||
|
DB::table('roles_menues')
|
||||||
|
->whereIn('menu_codigo', array_keys(self::SHOPIT_MENUS))
|
||||||
|
->delete();
|
||||||
|
DB::table('menues')
|
||||||
|
->whereIn('code', array_keys(self::SHOPIT_MENUS))
|
||||||
|
->delete();
|
||||||
|
|
||||||
|
foreach (self::ONTICKET_MENUS as $legacyCode => [$code, $label, $route]) {
|
||||||
|
$this->replaceMenuCode($code, $legacyCode, $label, $route);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($shopItTenantCodes as $tenantCode) {
|
||||||
|
foreach (self::LEGACY_SHOPIT_ADMIN_MENUS as $menuCode) {
|
||||||
|
$this->assignMenuToTenant($tenantCode, $menuCode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private function replaceMenuCode(
|
||||||
|
string $sourceCode,
|
||||||
|
string $targetCode,
|
||||||
|
string $label,
|
||||||
|
string $route,
|
||||||
|
): void {
|
||||||
|
$source = DB::table('menues')->where('code', $sourceCode)->first();
|
||||||
|
|
||||||
|
$this->upsertMenu($targetCode, $label, $route, $source);
|
||||||
|
|
||||||
|
if ($source === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
DB::table('tenants_menues')
|
||||||
|
->where('menu_code', $sourceCode)
|
||||||
|
->get()
|
||||||
|
->each(function (object $assignment) use ($targetCode): void {
|
||||||
|
DB::table('tenants_menues')->updateOrInsert(
|
||||||
|
[
|
||||||
|
'tenant_code' => $assignment->tenant_code,
|
||||||
|
'menu_code' => $targetCode,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'static_content' => $assignment->static_content,
|
||||||
|
'created_at' => $assignment->created_at ?? now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
DB::table('roles_menues')
|
||||||
|
->where('menu_codigo', $sourceCode)
|
||||||
|
->pluck('rol_codigo')
|
||||||
|
->each(fn (string $roleCode) => DB::table('roles_menues')->updateOrInsert([
|
||||||
|
'rol_codigo' => $roleCode,
|
||||||
|
'menu_codigo' => $targetCode,
|
||||||
|
]));
|
||||||
|
|
||||||
|
DB::table('tenants_menues')->where('menu_code', $sourceCode)->delete();
|
||||||
|
DB::table('roles_menues')->where('menu_codigo', $sourceCode)->delete();
|
||||||
|
DB::table('menues')->where('code', $sourceCode)->delete();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function upsertMenu(
|
||||||
|
string $code,
|
||||||
|
string $label,
|
||||||
|
string $route,
|
||||||
|
?object $source = null,
|
||||||
|
): void {
|
||||||
|
DB::table('menues')->updateOrInsert(
|
||||||
|
['code' => $code],
|
||||||
|
[
|
||||||
|
'label' => $source->label ?? $label,
|
||||||
|
'parent_menu_code' => 'main.adminapp',
|
||||||
|
'content_type' => $source->content_type ?? 'dynamic',
|
||||||
|
'static_content_schema' => $source->static_content_schema ?? null,
|
||||||
|
'route' => $source->route ?? $route,
|
||||||
|
'created_at' => $source->created_at ?? now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function assignMenuToAdminRoles(string $menuCode): void
|
||||||
|
{
|
||||||
|
DB::table('roles')
|
||||||
|
->whereIn('codigo', ['admin', 'adminapp'])
|
||||||
|
->pluck('codigo')
|
||||||
|
->each(fn (string $roleCode) => DB::table('roles_menues')->updateOrInsert([
|
||||||
|
'rol_codigo' => $roleCode,
|
||||||
|
'menu_codigo' => $menuCode,
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
|
||||||
|
private function assignMenuToTenant(string $tenantCode, string $menuCode): void
|
||||||
|
{
|
||||||
|
DB::table('tenants_menues')->updateOrInsert(
|
||||||
|
[
|
||||||
|
'tenant_code' => $tenantCode,
|
||||||
|
'menu_code' => $menuCode,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'static_content' => null,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('tenants_menues', function (Blueprint $table): void {
|
||||||
|
$table->boolean('is_enabled')->default(true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('tenants_menues', function (Blueprint $table): void {
|
||||||
|
$table->dropColumn('is_enabled');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
DB::table('menues')
|
||||||
|
->where('code', 'help.terms-and-conditions')
|
||||||
|
->update([
|
||||||
|
'content_type' => 'static',
|
||||||
|
'static_content_schema' => json_encode([
|
||||||
|
'html' => 'present|nullable|string',
|
||||||
|
], JSON_THROW_ON_ERROR),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
DB::table('menues')
|
||||||
|
->where('code', 'help.terms-and-conditions')
|
||||||
|
->update([
|
||||||
|
'content_type' => 'dynamic',
|
||||||
|
'static_content_schema' => null,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
$driver = DB::connection()->getDriverName();
|
||||||
|
|
||||||
|
if (! in_array($driver, ['mysql', 'mariadb', 'sqlite', 'pgsql', 'sqlsrv'], true)) {
|
||||||
|
throw new RuntimeException("Unsupported address database driver: {$driver}");
|
||||||
|
}
|
||||||
|
|
||||||
|
Schema::create('addresses', function (Blueprint $table): void {
|
||||||
|
$table->id();
|
||||||
|
$table->string('label');
|
||||||
|
$table->text('address_text');
|
||||||
|
$table->decimal('latitude', 10, 7)->nullable();
|
||||||
|
$table->decimal('longitude', 10, 7)->nullable();
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::create('tenant_addresses', function (Blueprint $table) use ($driver): void {
|
||||||
|
$table->id();
|
||||||
|
$table->string('tenant_codigo');
|
||||||
|
$table->foreignId('address_id')->constrained('addresses')->cascadeOnDelete();
|
||||||
|
$table->boolean('is_main')->default(false);
|
||||||
|
$table->timestamps();
|
||||||
|
|
||||||
|
$table->foreign('tenant_codigo')->references('codigo')->on('tenants')->cascadeOnDelete();
|
||||||
|
$table->unique(['tenant_codigo', 'address_id']);
|
||||||
|
|
||||||
|
if (in_array($driver, ['mysql', 'mariadb'], true)) {
|
||||||
|
// NULL allows many secondary addresses, while a tenant code can occur only once.
|
||||||
|
$table->string('main_tenant_codigo')->nullable()
|
||||||
|
->virtualAs('CASE WHEN is_main = 1 THEN tenant_codigo ELSE NULL END');
|
||||||
|
$table->unique('main_tenant_codigo', 'tenant_addresses_one_main_per_tenant');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (! in_array($driver, ['mysql', 'mariadb'], true)) {
|
||||||
|
$condition = $driver === 'pgsql' ? 'is_main = true' : 'is_main = 1';
|
||||||
|
DB::statement('CREATE UNIQUE INDEX tenant_addresses_one_main_per_tenant '
|
||||||
|
."ON tenant_addresses (tenant_codigo) WHERE {$condition}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('tenant_addresses');
|
||||||
|
Schema::dropIfExists('addresses');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
DB::transaction(function (): void {
|
||||||
|
DB::table('tenants_menues')
|
||||||
|
->where('menu_code', 'help.contact')
|
||||||
|
->whereNotNull('static_content')
|
||||||
|
->orderBy('id')
|
||||||
|
->chunkById(100, function ($menus): void {
|
||||||
|
foreach ($menus as $menu) {
|
||||||
|
$content = json_decode($menu->static_content, true, flags: JSON_THROW_ON_ERROR);
|
||||||
|
|
||||||
|
if ($content === null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! is_array($content) || ! is_array($content['locations'] ?? [])) {
|
||||||
|
throw new RuntimeException("Invalid contact locations for tenant {$menu->tenant_code}");
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($content['locations'] ?? [] as $location) {
|
||||||
|
if (! is_array($location) || ! is_array($location['addresses'] ?? [])) {
|
||||||
|
throw new RuntimeException("Invalid contact addresses for tenant {$menu->tenant_code}");
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($location['addresses'] ?? [] as $address) {
|
||||||
|
$this->copyAddress($menu->tenant_code, $location, $address);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private function copyAddress(string $tenantCode, array $location, mixed $address): void
|
||||||
|
{
|
||||||
|
if (! is_array($address) || ! is_string($address['address'] ?? null)
|
||||||
|
|| trim($address['address']) === '') {
|
||||||
|
throw new RuntimeException("Invalid contact address text for tenant {$tenantCode}");
|
||||||
|
}
|
||||||
|
|
||||||
|
$coordinates = $address['coordinates'] ?? null;
|
||||||
|
|
||||||
|
if ($coordinates !== null && (! is_array($coordinates) || count($coordinates) !== 2
|
||||||
|
|| ! is_numeric($coordinates[0] ?? null) || ! is_numeric($coordinates[1] ?? null)
|
||||||
|
|| ! is_finite((float) $coordinates[0]) || ! is_finite((float) $coordinates[1])
|
||||||
|
|| abs((float) $coordinates[0]) > 90 || abs((float) $coordinates[1]) > 180)) {
|
||||||
|
throw new RuntimeException("Invalid contact coordinates for tenant {$tenantCode}");
|
||||||
|
}
|
||||||
|
|
||||||
|
$label = $address['label'] ?? $location['label'] ?? 'Dirección';
|
||||||
|
|
||||||
|
if (! is_string($label) || trim($label) === '' || mb_strlen($label) > 255) {
|
||||||
|
throw new RuntimeException("Invalid contact address label for tenant {$tenantCode}");
|
||||||
|
}
|
||||||
|
|
||||||
|
$values = [
|
||||||
|
'label' => $label,
|
||||||
|
'address_text' => $address['address'],
|
||||||
|
'latitude' => $coordinates === null ? null : number_format((float) $coordinates[0], 7, '.', ''),
|
||||||
|
'longitude' => $coordinates === null ? null : number_format((float) $coordinates[1], 7, '.', ''),
|
||||||
|
];
|
||||||
|
|
||||||
|
// Reuse only within this tenant; equal legacy addresses in different tenants stay independent.
|
||||||
|
$existing = DB::table('addresses')
|
||||||
|
->join('tenant_addresses', 'tenant_addresses.address_id', '=', 'addresses.id')
|
||||||
|
->where('tenant_addresses.tenant_codigo', $tenantCode)
|
||||||
|
->where(array_combine(
|
||||||
|
array_map(fn (string $field): string => 'addresses.'.$field, array_keys($values)),
|
||||||
|
array_values($values),
|
||||||
|
))
|
||||||
|
->first(['addresses.id', 'tenant_addresses.id as link_id']);
|
||||||
|
|
||||||
|
if ($existing !== null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$now = now();
|
||||||
|
$isMain = ! DB::table('tenant_addresses')
|
||||||
|
->where('tenant_codigo', $tenantCode)
|
||||||
|
->where('is_main', true)
|
||||||
|
->exists();
|
||||||
|
$addressId = DB::table('addresses')->insertGetId([
|
||||||
|
...$values,
|
||||||
|
'created_at' => $now,
|
||||||
|
'updated_at' => $now,
|
||||||
|
]);
|
||||||
|
|
||||||
|
DB::table('tenant_addresses')->insert([
|
||||||
|
'tenant_codigo' => $tenantCode,
|
||||||
|
'address_id' => $addressId,
|
||||||
|
'is_main' => $isMain,
|
||||||
|
'created_at' => $now,
|
||||||
|
'updated_at' => $now,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
// Keep copies: they may already have been edited or reused. Legacy menus were never changed.
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('events', function (Blueprint $table): void {
|
||||||
|
$table->foreignId('address_id')->nullable()->constrained('addresses')->nullOnDelete();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('events', function (Blueprint $table): void {
|
||||||
|
$table->dropConstrainedForeignId('address_id');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
DB::transaction(function (): void {
|
||||||
|
DB::table('events')->whereNull('address_id')->orderBy('id')->lockForUpdate()
|
||||||
|
->chunkById(100, function ($events): void {
|
||||||
|
foreach ($events as $event) {
|
||||||
|
$coordinates = $event->exact_location === null
|
||||||
|
? null
|
||||||
|
: json_decode($event->exact_location, true, flags: JSON_THROW_ON_ERROR);
|
||||||
|
|
||||||
|
if ($coordinates !== null && (! is_array($coordinates)
|
||||||
|
|| ! is_numeric($coordinates['latitude'] ?? null)
|
||||||
|
|| ! is_numeric($coordinates['longitude'] ?? null)
|
||||||
|
|| ! is_finite((float) $coordinates['latitude'])
|
||||||
|
|| ! is_finite((float) $coordinates['longitude'])
|
||||||
|
|| abs((float) $coordinates['latitude']) > 90
|
||||||
|
|| abs((float) $coordinates['longitude']) > 180)) {
|
||||||
|
throw new RuntimeException("Invalid location coordinates for event {$event->id}");
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($event->location === null || trim($event->location) === '') {
|
||||||
|
if ($coordinates !== null) {
|
||||||
|
throw new RuntimeException("Missing address text for event {$event->id}");
|
||||||
|
}
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$now = now();
|
||||||
|
$addressId = DB::table('addresses')->insertGetId([
|
||||||
|
'label' => 'Ubicación del evento',
|
||||||
|
'address_text' => $event->location,
|
||||||
|
'latitude' => $coordinates === null ? null : $coordinates['latitude'],
|
||||||
|
'longitude' => $coordinates === null ? null : $coordinates['longitude'],
|
||||||
|
'created_at' => $now,
|
||||||
|
'updated_at' => $now,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Update only the new link, preserving the original fields and timestamps.
|
||||||
|
DB::table('events')->where('id', $event->id)->update(['address_id' => $addressId]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
// Copies may already have been edited or shared; keep them and the legacy data.
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
$driver = DB::connection()->getDriverName();
|
||||||
|
if (! in_array($driver, ['mysql', 'mariadb', 'sqlite', 'pgsql', 'sqlsrv'], true)) {
|
||||||
|
throw new RuntimeException("Unsupported phone number database driver: {$driver}");
|
||||||
|
}
|
||||||
|
|
||||||
|
Schema::create('phone_numbers', function (Blueprint $table): void {
|
||||||
|
$table->id();
|
||||||
|
$table->string('label');
|
||||||
|
$table->string('number');
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
Schema::create('tenant_phone_numbers', function (Blueprint $table) use ($driver): void {
|
||||||
|
$table->id();
|
||||||
|
$table->string('tenant_codigo');
|
||||||
|
$table->foreignId('phone_number_id')->constrained('phone_numbers')->cascadeOnDelete();
|
||||||
|
$table->boolean('is_main')->default(false);
|
||||||
|
$table->timestamps();
|
||||||
|
$table->foreign('tenant_codigo')->references('codigo')->on('tenants')->cascadeOnDelete();
|
||||||
|
$table->unique(['tenant_codigo', 'phone_number_id']);
|
||||||
|
if (in_array($driver, ['mysql', 'mariadb'], true)) {
|
||||||
|
$table->string('main_tenant_codigo')->nullable()
|
||||||
|
->virtualAs('CASE WHEN is_main = 1 THEN tenant_codigo ELSE NULL END');
|
||||||
|
$table->unique('main_tenant_codigo', 'tenant_phones_one_main');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (! in_array($driver, ['mysql', 'mariadb'], true)) {
|
||||||
|
$condition = $driver === 'pgsql' ? 'is_main = true' : 'is_main = 1';
|
||||||
|
DB::statement("CREATE UNIQUE INDEX tenant_phones_one_main ON tenant_phone_numbers (tenant_codigo) WHERE {$condition}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('tenant_phone_numbers');
|
||||||
|
Schema::dropIfExists('phone_numbers');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
DB::transaction(function (): void {
|
||||||
|
DB::table('tenants')->orderBy('id')->chunkById(100, function ($tenants): void {
|
||||||
|
foreach ($tenants as $tenant) {
|
||||||
|
$this->copyPhone($tenant->codigo, $tenant->phone, true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
DB::table('tenants_menues')->where('menu_code', 'help.contact')
|
||||||
|
->whereNotNull('static_content')->orderBy('id')->chunkById(100, function ($menus): void {
|
||||||
|
foreach ($menus as $menu) {
|
||||||
|
$content = json_decode($menu->static_content, true, flags: JSON_THROW_ON_ERROR);
|
||||||
|
if ($content === null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (! is_array($content)) {
|
||||||
|
throw new RuntimeException("Invalid contact content for tenant {$menu->tenant_code}");
|
||||||
|
}
|
||||||
|
$this->copyPhone($menu->tenant_code, $content['phone'] ?? null, false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private function copyPhone(string $tenantCode, mixed $number, bool $fromTenant): void
|
||||||
|
{
|
||||||
|
if ($number === null || (is_string($number) && trim($number) === '')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (! is_string($number) || mb_strlen($number) > 255 || ! preg_match('/[0-9]/', $number)) {
|
||||||
|
throw new RuntimeException("Invalid phone number for tenant {$tenantCode}");
|
||||||
|
}
|
||||||
|
$numbers = DB::table('phone_numbers')
|
||||||
|
->join('tenant_phone_numbers', 'phone_numbers.id', '=', 'tenant_phone_numbers.phone_number_id')
|
||||||
|
->where('tenant_codigo', $tenantCode)->get(['phone_numbers.number']);
|
||||||
|
$dialable = preg_replace('/[^0-9]/', '', $number);
|
||||||
|
foreach ($numbers as $existing) {
|
||||||
|
if (preg_replace('/[^0-9]/', '', $existing->number) === $dialable) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$isMain = ! DB::table('tenant_phone_numbers')->where('tenant_codigo', $tenantCode)->where('is_main', true)->exists();
|
||||||
|
$now = now();
|
||||||
|
$id = DB::table('phone_numbers')->insertGetId([
|
||||||
|
'label' => $fromTenant ? 'Principal' : 'Contacto',
|
||||||
|
'number' => trim($number), 'created_at' => $now, 'updated_at' => $now,
|
||||||
|
]);
|
||||||
|
DB::table('tenant_phone_numbers')->insert([
|
||||||
|
'tenant_codigo' => $tenantCode, 'phone_number_id' => $id, 'is_main' => $isMain,
|
||||||
|
'created_at' => $now, 'updated_at' => $now,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
// Preserve copies that may have been edited. The legacy sources were not changed.
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
DB::table('menues')->where('code', 'help.contact')->update([
|
||||||
|
'content_type' => 'dynamic',
|
||||||
|
'static_content_schema' => null,
|
||||||
|
]);
|
||||||
|
// Preserve legacy pivot content for rollback; dynamic menus do not expose it.
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
DB::table('menues')->where('code', 'help.contact')->update([
|
||||||
|
'content_type' => 'static',
|
||||||
|
'static_content_schema' => json_encode([
|
||||||
|
'whatsapp' => 'required|array',
|
||||||
|
'whatsapp.whatsapp_url' => 'required|url',
|
||||||
|
'whatsapp.whatsapp_label' => 'required|string',
|
||||||
|
'phone' => 'required|string',
|
||||||
|
'locations' => 'required|array|min:1',
|
||||||
|
'locations.*.label' => 'required|string',
|
||||||
|
'locations.*.addresses' => 'required|array|min:1',
|
||||||
|
'locations.*.addresses.*' => 'required|array:label,address,coordinates',
|
||||||
|
'locations.*.addresses.*.label' => 'required|string',
|
||||||
|
'locations.*.addresses.*.address' => 'required|string',
|
||||||
|
'locations.*.addresses.*.coordinates' => 'required|array|size:2',
|
||||||
|
'locations.*.addresses.*.coordinates.0' => 'required|numeric|between:-90,90',
|
||||||
|
'locations.*.addresses.*.coordinates.1' => 'required|numeric|between:-180,180',
|
||||||
|
'map_locations' => 'prohibited',
|
||||||
|
], JSON_THROW_ON_ERROR),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('phone_numbers', function (Blueprint $table): void {
|
||||||
|
$table->string('country_code', 8)->nullable()->default('+54');
|
||||||
|
$table->string('mobile_prefix', 4)->nullable()->default('9');
|
||||||
|
$table->string('area_code', 8)->nullable();
|
||||||
|
$table->string('local_number')->nullable();
|
||||||
|
});
|
||||||
|
|
||||||
|
DB::table('phone_numbers')->orderBy('id')->chunkById(100, function ($numbers): void {
|
||||||
|
foreach ($numbers as $phone) {
|
||||||
|
DB::table('phone_numbers')->where('id', $phone->id)
|
||||||
|
->update($this->components($phone->number));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::table('phone_numbers', function (Blueprint $table): void {
|
||||||
|
$table->dropColumn('number');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('phone_numbers', function (Blueprint $table): void {
|
||||||
|
$table->string('number')->nullable();
|
||||||
|
});
|
||||||
|
|
||||||
|
DB::table('phone_numbers')->orderBy('id')->chunkById(100, function ($numbers): void {
|
||||||
|
foreach ($numbers as $phone) {
|
||||||
|
$parts = array_filter([
|
||||||
|
$phone->country_code,
|
||||||
|
$phone->mobile_prefix,
|
||||||
|
$phone->area_code,
|
||||||
|
$phone->local_number,
|
||||||
|
], fn ($part): bool => $part !== null && $part !== '');
|
||||||
|
DB::table('phone_numbers')->where('id', $phone->id)
|
||||||
|
->update(['number' => implode(' ', $parts)]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::table('phone_numbers', function (Blueprint $table): void {
|
||||||
|
$table->dropColumn(['country_code', 'mobile_prefix', 'area_code', 'local_number']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return array{country_code: ?string, mobile_prefix: ?string, area_code: ?string, local_number: string} */
|
||||||
|
private function components(string $number): array
|
||||||
|
{
|
||||||
|
$number = trim($number);
|
||||||
|
|
||||||
|
if (preg_match('/^(\+?\d{1,3})\s+(?:(9)\s+)?\(?(\d{2,5})\)?[\s-]+([\d\s-]+)$/', $number, $match)) {
|
||||||
|
return [
|
||||||
|
'country_code' => '+'.ltrim($match[1], '+'),
|
||||||
|
'mobile_prefix' => ($match[2] ?? '') ?: null,
|
||||||
|
'area_code' => $match[3],
|
||||||
|
'local_number' => preg_replace('/\D/', '', $match[4]),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (preg_match('/^\(?(0\d{2,5})\)?[\s-]+([\d\s-]+)$/', $number, $match)) {
|
||||||
|
return [
|
||||||
|
'country_code' => null,
|
||||||
|
'mobile_prefix' => null,
|
||||||
|
'area_code' => $match[1],
|
||||||
|
'local_number' => preg_replace('/\D/', '', $match[2]),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (preg_match('/^\+54(9?)(\d+)$/', $number, $match)) {
|
||||||
|
return [
|
||||||
|
'country_code' => '+54',
|
||||||
|
'mobile_prefix' => $match[1] === '9' ? '9' : null,
|
||||||
|
'area_code' => null,
|
||||||
|
'local_number' => $match[2],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'country_code' => null,
|
||||||
|
'mobile_prefix' => null,
|
||||||
|
'area_code' => null,
|
||||||
|
'local_number' => $number,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -6,6 +6,7 @@ use App\Domains\Commerce\Catalog\Enums\InventoryPolicy;
|
|||||||
use App\Domains\Commerce\Catalog\Models\Attribute;
|
use App\Domains\Commerce\Catalog\Models\Attribute;
|
||||||
use App\Domains\Commerce\Catalog\Models\CatalogItem;
|
use App\Domains\Commerce\Catalog\Models\CatalogItem;
|
||||||
use App\Domains\Commerce\Catalog\Services\CatalogService;
|
use App\Domains\Commerce\Catalog\Services\CatalogService;
|
||||||
|
use App\Domains\Core\Client\Models\Client;
|
||||||
use App\Domains\Core\Tenant\Models\Tenant;
|
use App\Domains\Core\Tenant\Models\Tenant;
|
||||||
use App\Domains\Ticketing\Event\Models\Event;
|
use App\Domains\Ticketing\Event\Models\Event;
|
||||||
use App\Shared\Attachable\Services\AttachmentService;
|
use App\Shared\Attachable\Services\AttachmentService;
|
||||||
@@ -13,6 +14,7 @@ use App\Shared\Enums\FieldType;
|
|||||||
use Illuminate\Database\Seeder;
|
use Illuminate\Database\Seeder;
|
||||||
use Illuminate\Http\UploadedFile;
|
use Illuminate\Http\UploadedFile;
|
||||||
use Illuminate\Support\Carbon;
|
use Illuminate\Support\Carbon;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
use Illuminate\Support\Facades\Storage;
|
use Illuminate\Support\Facades\Storage;
|
||||||
use RuntimeException;
|
use RuntimeException;
|
||||||
|
|
||||||
@@ -35,6 +37,12 @@ class FiestaTradicionArrufoSeeder extends Seeder
|
|||||||
throw new RuntimeException("Tenant 'onticket' no encontrado.");
|
throw new RuntimeException("Tenant 'onticket' no encontrado.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$client = Client::query()->where('code', 'pyme_rural')->first();
|
||||||
|
|
||||||
|
if ($client === null) {
|
||||||
|
throw new RuntimeException("Client 'pyme_rural' no encontrado.");
|
||||||
|
}
|
||||||
|
|
||||||
Attribute::query()->firstOrCreate(
|
Attribute::query()->firstOrCreate(
|
||||||
['tenant_codigo' => $tenant->codigo, 'codigo' => 'event_date'],
|
['tenant_codigo' => $tenant->codigo, 'codigo' => 'event_date'],
|
||||||
['nombre' => 'Fecha', 'type' => FieldType::EventDate, 'is_required' => true],
|
['nombre' => 'Fecha', 'type' => FieldType::EventDate, 'is_required' => true],
|
||||||
@@ -44,13 +52,19 @@ class FiestaTradicionArrufoSeeder extends Seeder
|
|||||||
['tenant_code' => $tenant->codigo, 'title' => self::TITLE],
|
['tenant_code' => $tenant->codigo, 'title' => self::TITLE],
|
||||||
['published_at' => now()],
|
['published_at' => now()],
|
||||||
);
|
);
|
||||||
$event->update([
|
$eventData = [
|
||||||
'subtitle' => 'Una noche para celebrar nuestras raíces y mantener viva la tradición gaucha.',
|
'subtitle' => 'Una noche para celebrar nuestras raíces y mantener viva la tradición gaucha.',
|
||||||
'description' => 'La 26.º Fiesta de la Tradición y 4.º Encuentro de Agrupaciones Gauchas reunirá a agrupaciones, artesanos, pilcheros y público en general para compartir una jornada dedicada a nuestras costumbres y cultura.'
|
'description' => 'La 26.º Fiesta de la Tradición y 4.º Encuentro de Agrupaciones Gauchas reunirá a agrupaciones, artesanos, pilcheros y público en general para compartir una jornada dedicada a nuestras costumbres y cultura.'
|
||||||
."\n\n".'Un encuentro para disfrutar de la tradición, la identidad gaucha y el espíritu de camaradería, en el Predio de Doma del Club Unión Deportiva Arrufó.'
|
."\n\n".'Un encuentro para disfrutar de la tradición, la identidad gaucha y el espíritu de camaradería, en el Predio de Doma del Club Unión Deportiva Arrufó.'
|
||||||
."\n\n".'Organiza: Biblioteca Popular Miguel Ángel Sosa.',
|
."\n\n".'Organiza: Biblioteca Popular Miguel Ángel Sosa.',
|
||||||
'location' => 'Predio de Doma del Club Unión Deportiva Arrufó',
|
'location' => 'Predio de Doma del Club Unión Deportiva Arrufó',
|
||||||
]);
|
];
|
||||||
|
|
||||||
|
if (Schema::hasColumn('events', 'client_id')) {
|
||||||
|
$eventData['client_id'] = $client->id;
|
||||||
|
}
|
||||||
|
|
||||||
|
$event->update($eventData);
|
||||||
|
|
||||||
$eventImage = $event->attachment;
|
$eventImage = $event->attachment;
|
||||||
if ($eventImage === null || ! Storage::disk('s3')->exists($eventImage->path)) {
|
if ($eventImage === null || ! Storage::disk('s3')->exists($eventImage->path)) {
|
||||||
|
|||||||
@@ -53,89 +53,131 @@ class MenuSeeder extends Seeder
|
|||||||
'route' => '/scanner/detail/:id',
|
'route' => '/scanner/detail/:id',
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'code' => 'adminapp.event',
|
'code' => 'onticket.adminapp.event',
|
||||||
'label' => 'Eventos',
|
'label' => 'Eventos',
|
||||||
'parent_menu_code' => 'main.adminapp',
|
'parent_menu_code' => 'main.adminapp',
|
||||||
'route' => '/admin/event',
|
'route' => '/admin/event',
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'code' => 'adminapp.inicio',
|
'code' => 'onticket.adminapp.inicio',
|
||||||
'label' => 'Inicio',
|
'label' => 'Inicio',
|
||||||
'parent_menu_code' => 'main.adminapp',
|
'parent_menu_code' => 'main.adminapp',
|
||||||
'route' => '/admin/inicio',
|
'route' => '/admin/inicio',
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'code' => 'adminapp.catalog',
|
'code' => 'onticket.adminapp.catalog',
|
||||||
'label' => 'Catálogo',
|
'label' => 'Catálogo',
|
||||||
'parent_menu_code' => 'main.adminapp',
|
'parent_menu_code' => 'main.adminapp',
|
||||||
'route' => '/admin/catalog',
|
'route' => '/admin/catalog',
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'code' => 'adminapp.combos',
|
'code' => 'onticket.adminapp.combos',
|
||||||
'label' => 'Combos',
|
'label' => 'Combos',
|
||||||
'parent_menu_code' => 'main.adminapp',
|
'parent_menu_code' => 'main.adminapp',
|
||||||
'route' => '/admin/combos',
|
'route' => '/admin/combos',
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'code' => 'adminapp.categories',
|
'code' => 'onticket.adminapp.categories',
|
||||||
'label' => 'Categorías',
|
'label' => 'Categorías',
|
||||||
'parent_menu_code' => 'main.adminapp',
|
'parent_menu_code' => 'main.adminapp',
|
||||||
'route' => '/admin/categories',
|
'route' => '/admin/categories',
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'code' => 'adminapp.ventas',
|
'code' => 'onticket.adminapp.ventas',
|
||||||
'label' => 'Ventas',
|
'label' => 'Ventas',
|
||||||
'parent_menu_code' => 'main.adminapp',
|
'parent_menu_code' => 'main.adminapp',
|
||||||
'route' => '/admin/ventas',
|
'route' => '/admin/ventas',
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'code' => 'adminapp.staff',
|
'code' => 'onticket.adminapp.staff',
|
||||||
'label' => 'Usuarios',
|
'label' => 'Usuarios',
|
||||||
'parent_menu_code' => 'main.adminapp',
|
'parent_menu_code' => 'main.adminapp',
|
||||||
'route' => '/admin/staff',
|
'route' => '/admin/staff',
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'code' => 'adminapp.tickets',
|
'code' => 'onticket.adminapp.tickets',
|
||||||
'label' => 'Tickets',
|
'label' => 'Tickets',
|
||||||
'parent_menu_code' => 'main.adminapp',
|
'parent_menu_code' => 'main.adminapp',
|
||||||
'route' => '/admin/tickets',
|
'route' => '/admin/tickets',
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'code' => 'adminapp.fiesta-futbol-infantil.entradas',
|
'code' => 'onticket.adminapp.fiesta-futbol-infantil.entradas',
|
||||||
'label' => 'Entradas',
|
'label' => 'Entradas',
|
||||||
'parent_menu_code' => 'main.adminapp',
|
'parent_menu_code' => 'main.adminapp',
|
||||||
'route' => '/admin/entradas',
|
'route' => '/admin/entradas',
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'code' => 'adminapp.fiesta-futbol-infantil.alojamientos',
|
'code' => 'onticket.adminapp.fiesta-futbol-infantil.alojamientos',
|
||||||
'label' => 'Alojamientos',
|
'label' => 'Alojamientos',
|
||||||
'parent_menu_code' => 'main.adminapp',
|
'parent_menu_code' => 'main.adminapp',
|
||||||
'route' => '/admin/alojamientos',
|
'route' => '/admin/alojamientos',
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'code' => 'adminapp.fiesta-futbol-infantil.merchandising',
|
'code' => 'onticket.adminapp.fiesta-futbol-infantil.merchandising',
|
||||||
'label' => 'Merchandising',
|
'label' => 'Merchandising',
|
||||||
'parent_menu_code' => 'main.adminapp',
|
'parent_menu_code' => 'main.adminapp',
|
||||||
'route' => '/admin/merchandising',
|
'route' => '/admin/merchandising',
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'code' => 'adminapp.fiesta-futbol-infantil.comida',
|
'code' => 'onticket.adminapp.fiesta-futbol-infantil.comida',
|
||||||
'label' => 'Comida',
|
'label' => 'Comida',
|
||||||
'parent_menu_code' => 'main.adminapp',
|
'parent_menu_code' => 'main.adminapp',
|
||||||
'route' => '/admin/comidas',
|
'route' => '/admin/comidas',
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'code' => 'adminapp.desfile.entradas',
|
'code' => 'onticket.adminapp.desfile.entradas',
|
||||||
'label' => 'Entradas',
|
'label' => 'Entradas',
|
||||||
'parent_menu_code' => 'main.adminapp',
|
'parent_menu_code' => 'main.adminapp',
|
||||||
'route' => '/admin/desfile/entradas',
|
'route' => '/admin/desfile/entradas',
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'code' => 'adminapp.desfile.reservas',
|
'code' => 'onticket.adminapp.desfile.reservas',
|
||||||
'label' => 'Reserva de Tickets',
|
'label' => 'Reserva de Tickets',
|
||||||
'parent_menu_code' => 'main.adminapp',
|
'parent_menu_code' => 'main.adminapp',
|
||||||
'route' => '/admin/desfile/reservas',
|
'route' => '/admin/desfile/reservas',
|
||||||
],
|
],
|
||||||
|
[
|
||||||
|
'code' => 'shopit.adminapp.marca',
|
||||||
|
'label' => 'Marca',
|
||||||
|
'parent_menu_code' => 'main.adminapp',
|
||||||
|
'route' => '/admin/marca',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'code' => 'shopit.adminapp.contacto',
|
||||||
|
'label' => 'Contacto',
|
||||||
|
'parent_menu_code' => 'main.adminapp',
|
||||||
|
'route' => '/admin/contacto',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'code' => 'shopit.adminapp.productos',
|
||||||
|
'label' => 'Productos',
|
||||||
|
'parent_menu_code' => 'main.adminapp',
|
||||||
|
'route' => '/admin/productos',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'code' => 'shopit.adminapp.categorias',
|
||||||
|
'label' => 'Categorías',
|
||||||
|
'parent_menu_code' => 'main.adminapp',
|
||||||
|
'route' => '/admin/categorias',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'code' => 'shopit.adminapp.transacciones',
|
||||||
|
'label' => 'Transacciones',
|
||||||
|
'parent_menu_code' => 'main.adminapp',
|
||||||
|
'route' => '/admin/transacciones',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'code' => 'shopit.adminapp.reportes',
|
||||||
|
'label' => 'Reportes',
|
||||||
|
'parent_menu_code' => 'main.adminapp',
|
||||||
|
'route' => '/admin/reportes',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'code' => 'shopit.adminapp.usuarios_roles',
|
||||||
|
'label' => 'Usuarios y roles',
|
||||||
|
'parent_menu_code' => 'main.adminapp',
|
||||||
|
'route' => '/admin/usuarios-roles',
|
||||||
|
],
|
||||||
[
|
[
|
||||||
'code' => 'account',
|
'code' => 'account',
|
||||||
'label' => 'Mi cuenta',
|
'label' => 'Mi cuenta',
|
||||||
@@ -182,23 +224,8 @@ class MenuSeeder extends Seeder
|
|||||||
'code' => 'help.contact',
|
'code' => 'help.contact',
|
||||||
'label' => 'Contacto',
|
'label' => 'Contacto',
|
||||||
'parent_menu_code' => 'help',
|
'parent_menu_code' => 'help',
|
||||||
'content_type' => Menu::CONTENT_TYPE_STATIC,
|
'content_type' => Menu::CONTENT_TYPE_DYNAMIC,
|
||||||
'static_content_schema' => [
|
'static_content_schema' => null,
|
||||||
'whatsapp' => 'required|array',
|
|
||||||
'whatsapp.whatsapp_url' => 'required|url',
|
|
||||||
'whatsapp.whatsapp_label' => 'required|string',
|
|
||||||
'phone' => 'required|string',
|
|
||||||
'locations' => 'required|array|min:1',
|
|
||||||
'locations.*.label' => 'required|string',
|
|
||||||
'locations.*.addresses' => 'required|array|min:1',
|
|
||||||
'locations.*.addresses.*' => 'required|array:label,address,coordinates',
|
|
||||||
'locations.*.addresses.*.label' => 'required|string',
|
|
||||||
'locations.*.addresses.*.address' => 'required|string',
|
|
||||||
'locations.*.addresses.*.coordinates' => 'required|array|size:2',
|
|
||||||
'locations.*.addresses.*.coordinates.0' => 'required|numeric|between:-90,90',
|
|
||||||
'locations.*.addresses.*.coordinates.1' => 'required|numeric|between:-180,180',
|
|
||||||
'map_locations' => 'prohibited',
|
|
||||||
],
|
|
||||||
'route' => '/ayuda/contacto',
|
'route' => '/ayuda/contacto',
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
@@ -219,7 +246,10 @@ class MenuSeeder extends Seeder
|
|||||||
'code' => 'help.terms-and-conditions',
|
'code' => 'help.terms-and-conditions',
|
||||||
'label' => 'Términos y condiciones',
|
'label' => 'Términos y condiciones',
|
||||||
'parent_menu_code' => 'help',
|
'parent_menu_code' => 'help',
|
||||||
'content_type' => Menu::CONTENT_TYPE_DYNAMIC,
|
'content_type' => Menu::CONTENT_TYPE_STATIC,
|
||||||
|
'static_content_schema' => [
|
||||||
|
'html' => 'present|nullable|string',
|
||||||
|
],
|
||||||
'route' => '/ayuda/terminos-y-condiciones',
|
'route' => '/ayuda/terminos-y-condiciones',
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
@@ -255,6 +285,10 @@ class MenuSeeder extends Seeder
|
|||||||
])
|
])
|
||||||
->delete();
|
->delete();
|
||||||
|
|
||||||
|
Menu::query()
|
||||||
|
->where('code', 'like', 'adminapp.%')
|
||||||
|
->delete();
|
||||||
|
|
||||||
$allRoleMenuCodes = Menu::query()->pluck('code');
|
$allRoleMenuCodes = Menu::query()->pluck('code');
|
||||||
$adminAppMenuCodes = Menu::query()
|
$adminAppMenuCodes = Menu::query()
|
||||||
->where('code', 'main.adminapp')
|
->where('code', 'main.adminapp')
|
||||||
@@ -299,24 +333,32 @@ class MenuSeeder extends Seeder
|
|||||||
'mutual_smep',
|
'mutual_smep',
|
||||||
];
|
];
|
||||||
$ticketAdminMenuCodes = [
|
$ticketAdminMenuCodes = [
|
||||||
'adminapp.tickets',
|
'onticket.adminapp.tickets',
|
||||||
];
|
];
|
||||||
$fiestaCategoryMenuCodes = [
|
$fiestaCategoryMenuCodes = [
|
||||||
'adminapp.fiesta-futbol-infantil.entradas',
|
'onticket.adminapp.fiesta-futbol-infantil.entradas',
|
||||||
'adminapp.fiesta-futbol-infantil.alojamientos',
|
'onticket.adminapp.fiesta-futbol-infantil.alojamientos',
|
||||||
'adminapp.fiesta-futbol-infantil.merchandising',
|
'onticket.adminapp.fiesta-futbol-infantil.merchandising',
|
||||||
'adminapp.fiesta-futbol-infantil.comida',
|
'onticket.adminapp.fiesta-futbol-infantil.comida',
|
||||||
];
|
];
|
||||||
$fiestaExcludedAdminMenuCodes = [
|
$fiestaExcludedAdminMenuCodes = [
|
||||||
'adminapp.inicio',
|
'onticket.adminapp.inicio',
|
||||||
'adminapp.catalog',
|
'onticket.adminapp.catalog',
|
||||||
'adminapp.categories',
|
'onticket.adminapp.categories',
|
||||||
'adminapp.combos',
|
'onticket.adminapp.combos',
|
||||||
];
|
];
|
||||||
$desfileMenuCodes = [
|
$desfileMenuCodes = [
|
||||||
'adminapp.desfile.entradas',
|
'onticket.adminapp.desfile.entradas',
|
||||||
'adminapp.desfile.reservas',
|
'onticket.adminapp.desfile.reservas',
|
||||||
];
|
];
|
||||||
|
$shopItAdminMenuCodes = Menu::query()
|
||||||
|
->where('code', 'like', 'shopit.adminapp.%')
|
||||||
|
->pluck('code')
|
||||||
|
->all();
|
||||||
|
$onTicketAdminMenuCodes = Menu::query()
|
||||||
|
->where('code', 'like', 'onticket.adminapp.%')
|
||||||
|
->pluck('code')
|
||||||
|
->all();
|
||||||
$onTicketMenuCodes = [
|
$onTicketMenuCodes = [
|
||||||
'event.index',
|
'event.index',
|
||||||
'event.category',
|
'event.category',
|
||||||
@@ -342,30 +384,6 @@ class MenuSeeder extends Seeder
|
|||||||
'is_active' => false,
|
'is_active' => false,
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
$contactContent = [
|
|
||||||
'whatsapp' => [
|
|
||||||
'whatsapp_url' => 'https://wa.me/543412602222',
|
|
||||||
'whatsapp_label' => 'Chatea con nosotros',
|
|
||||||
],
|
|
||||||
'phone' => '+54 9 (0341) 6658247',
|
|
||||||
'locations' => [
|
|
||||||
'rosario' => [
|
|
||||||
'label' => 'Rosario',
|
|
||||||
'addresses' => [
|
|
||||||
[
|
|
||||||
'label' => 'Gigante de Arroyito',
|
|
||||||
'address' => 'Av. Génova 640, Rosario',
|
|
||||||
'coordinates' => [-32.913997, -60.674567],
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'label' => 'Telepagos',
|
|
||||||
'address' => 'Rioja 1150, piso 12, dpto. 3, Rosario',
|
|
||||||
'coordinates' => [-32.946820, -60.639320],
|
|
||||||
],
|
|
||||||
],
|
|
||||||
],
|
|
||||||
],
|
|
||||||
];
|
|
||||||
$mutualSmepFrequentlyAskedQuestions = [
|
$mutualSmepFrequentlyAskedQuestions = [
|
||||||
[
|
[
|
||||||
'pregunta' => '¿Hay algún límite de compra?',
|
'pregunta' => '¿Hay algún límite de compra?',
|
||||||
@@ -383,29 +401,17 @@ class MenuSeeder extends Seeder
|
|||||||
'is_active' => false,
|
'is_active' => false,
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
$mutualSmepContactContent = [
|
|
||||||
'whatsapp' => [
|
|
||||||
'whatsapp_url' => 'https://wa.me/5493412474530',
|
|
||||||
'whatsapp_label' => 'Chateá con Mutual SMEP',
|
|
||||||
],
|
|
||||||
'phone' => '+54 9 341 247-4530',
|
|
||||||
'locations' => [
|
|
||||||
'rosario' => [
|
|
||||||
'label' => 'Rosario',
|
|
||||||
'addresses' => [
|
|
||||||
[
|
|
||||||
'label' => 'Mutual SMEP',
|
|
||||||
'address' => 'San Lorenzo 1543, Rosario, Santa Fe',
|
|
||||||
'coordinates' => [-32.9431184, -60.6437991],
|
|
||||||
],
|
|
||||||
],
|
|
||||||
],
|
|
||||||
],
|
|
||||||
];
|
|
||||||
|
|
||||||
foreach ($tenants as $tenant) {
|
foreach ($tenants as $tenant) {
|
||||||
$menuCodes = $allMenus;
|
$menuCodes = $allMenus;
|
||||||
|
|
||||||
|
$menuCodes = array_diff(
|
||||||
|
$menuCodes,
|
||||||
|
$tenant->admin_website_type_code === 'shopit'
|
||||||
|
? $onTicketAdminMenuCodes
|
||||||
|
: $shopItAdminMenuCodes,
|
||||||
|
);
|
||||||
|
|
||||||
if ($tenant->codigo === 'sonder') {
|
if ($tenant->codigo === 'sonder') {
|
||||||
// Sonder NO tiene tickets
|
// Sonder NO tiene tickets
|
||||||
$menuCodes = array_diff($menuCodes, ['account.tickets']);
|
$menuCodes = array_diff($menuCodes, ['account.tickets']);
|
||||||
@@ -447,11 +453,6 @@ class MenuSeeder extends Seeder
|
|||||||
? $mutualSmepFrequentlyAskedQuestions
|
? $mutualSmepFrequentlyAskedQuestions
|
||||||
: $frequentlyAskedQuestions,
|
: $frequentlyAskedQuestions,
|
||||||
]);
|
]);
|
||||||
$tenant->menues()->updateExistingPivot('help.contact', [
|
|
||||||
'static_content' => $tenant->codigo === 'mutual_smep'
|
|
||||||
? $mutualSmepContactContent
|
|
||||||
: $contactContent,
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ class WebsiteTypeSeeder extends Seeder
|
|||||||
|
|
||||||
public function run(): void
|
public function run(): void
|
||||||
{
|
{
|
||||||
$shopIt = $this->websiteTypeService->updateOrCreate(
|
$this->websiteTypeService->updateOrCreate(
|
||||||
['codigo' => 'shopit'],
|
['codigo' => 'shopit'],
|
||||||
[
|
[
|
||||||
'nombre' => 'ShopIt',
|
'nombre' => 'ShopIt',
|
||||||
@@ -48,9 +48,10 @@ class WebsiteTypeSeeder extends Seeder
|
|||||||
'scanner_domain' => 'scanner.localhost',
|
'scanner_domain' => 'scanner.localhost',
|
||||||
'site_title' => 'ShopIt',
|
'site_title' => 'ShopIt',
|
||||||
...self::PRESENTATION,
|
...self::PRESENTATION,
|
||||||
'site_logo' => $this->onTicketLogo(),
|
'primary_color' => '#2FD3AC',
|
||||||
'footer_logo' => $this->onTicketFooterLogo(),
|
'site_logo' => $this->shopItLogo(),
|
||||||
'favicon' => $this->onTicketFavicon(),
|
'footer_logo' => $this->shopItFooterLogo(),
|
||||||
|
'favicon' => $this->shopItFavicon(),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -83,7 +84,7 @@ class WebsiteTypeSeeder extends Seeder
|
|||||||
...self::PRESENTATION,
|
...self::PRESENTATION,
|
||||||
'site_logo' => $this->onTicketLogo(),
|
'site_logo' => $this->onTicketLogo(),
|
||||||
'footer_logo' => $this->onTicketFooterLogo(),
|
'footer_logo' => $this->onTicketFooterLogo(),
|
||||||
'favicon' => $shopIt->favicon()->firstOrFail()->key,
|
'favicon' => $this->onTicketFavicon(),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -204,7 +205,7 @@ class WebsiteTypeSeeder extends Seeder
|
|||||||
|
|
||||||
private function onTicketLogo(): UploadedFile
|
private function onTicketLogo(): UploadedFile
|
||||||
{
|
{
|
||||||
$path = public_path('images/website_types/onticket_logo.png');
|
$path = public_path('images/website_types/onticket/onticket_logo.png');
|
||||||
|
|
||||||
if (! file_exists($path)) {
|
if (! file_exists($path)) {
|
||||||
throw new RuntimeException("OnTicket logo not found at path: {$path}");
|
throw new RuntimeException("OnTicket logo not found at path: {$path}");
|
||||||
@@ -221,7 +222,7 @@ class WebsiteTypeSeeder extends Seeder
|
|||||||
|
|
||||||
private function onTicketFooterLogo(): UploadedFile
|
private function onTicketFooterLogo(): UploadedFile
|
||||||
{
|
{
|
||||||
$path = public_path('images/website_types/onticket_footer_logo.png');
|
$path = public_path('images/website_types/onticket/onticket_footer_logo.png');
|
||||||
|
|
||||||
if (! file_exists($path)) {
|
if (! file_exists($path)) {
|
||||||
throw new RuntimeException("OnTicket footer logo not found at path: {$path}");
|
throw new RuntimeException("OnTicket footer logo not found at path: {$path}");
|
||||||
@@ -238,7 +239,7 @@ class WebsiteTypeSeeder extends Seeder
|
|||||||
|
|
||||||
private function onTicketFavicon(): UploadedFile
|
private function onTicketFavicon(): UploadedFile
|
||||||
{
|
{
|
||||||
$path = public_path('images/website_types/onticket_favicon.svg');
|
$path = public_path('images/website_types/onticket/onticket_favicon.svg');
|
||||||
|
|
||||||
if (! file_exists($path)) {
|
if (! file_exists($path)) {
|
||||||
throw new RuntimeException("OnTicket favicon not found at path: {$path}");
|
throw new RuntimeException("OnTicket favicon not found at path: {$path}");
|
||||||
@@ -252,4 +253,30 @@ class WebsiteTypeSeeder extends Seeder
|
|||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function shopItLogo(): UploadedFile
|
||||||
|
{
|
||||||
|
return $this->uploadedImage('shopit', 'shopit_logo.png', 'image/png');
|
||||||
|
}
|
||||||
|
|
||||||
|
private function shopItFooterLogo(): UploadedFile
|
||||||
|
{
|
||||||
|
return $this->uploadedImage('shopit', 'shopit_footer_logo.png', 'image/png');
|
||||||
|
}
|
||||||
|
|
||||||
|
private function shopItFavicon(): UploadedFile
|
||||||
|
{
|
||||||
|
return $this->uploadedImage('shopit', 'shopit-favicon.svg', 'image/svg+xml');
|
||||||
|
}
|
||||||
|
|
||||||
|
private function uploadedImage(string $websiteType, string $filename, string $mimeType): UploadedFile
|
||||||
|
{
|
||||||
|
$path = public_path("images/website_types/{$websiteType}/{$filename}");
|
||||||
|
|
||||||
|
if (! file_exists($path)) {
|
||||||
|
throw new RuntimeException("Website type image not found at path: {$path}");
|
||||||
|
}
|
||||||
|
|
||||||
|
return new UploadedFile($path, $filename, $mimeType, null, true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 422 B After Width: | Height: | Size: 422 B |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 3.0 KiB |
10
public/images/website_types/shopit/shopit-favicon.svg
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<svg width="41" height="41" viewBox="0 0 41 41" fill="none" xmlns="http://www.w3.org/2000/svg" xmlns:se="http://svg-edit.googlecode.com" se:swatches="W3siaWQiOiJCMDBEREFFMy0zMjUwLTRERDgtOEU0RS0zMDYxREJGNUE3QTMiLCJuYW1lIjoiIzVCNUI1QiIsImhleCI6IiM1QjVCNUIifSx7ImlkIjoiRjI4QjU4NTYtNUM2OC00QkY4LTk3MDMtOTk5NDJCOTgzNTVBIiwibmFtZSI6IiMyRkQzQUMiLCJoZXgiOiIjMkZEM0FDIn1d">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<g><se:title>Layer 1</se:title><path d="m16.32,27.13c2.29,0 3.13,-0.37 3.73,-0.76c0.65,-0.42 1.17,-1.06 1.24,-1.97c0.06,-0.76 -0.29,-1.4 -0.81,-1.78c-0.7,-0.5 -2.05,-0.81 -3.21,-1.05c-1.13,-0.24 -2.14,-0.6 -3.03,-1.22c-1.06,-0.73 -1.61,-1.47 -1.96,-2.52c-0.45,-1.32 -0.36,-3.12 -0.11,-4.02c0.43,-1.54 1.35,-2.86 2.59,-3.84001c1.6,-1.26 3.58,-2.01999 6.46,-2.17999c0.3,-0.02 0.71,-0.04 1.21,-0.04c0,0 0.16,0 0.21,0h13.3c-3.68,-4.71 -9.41,-7.75 -15.85,-7.75c-11.1,0 -20.09,9 -20.09,20.09c0,2.47 0.45,4.84 1.27,7.03h15.06l-0.01,0.01z" fill="#2FD3AC" id="svg_7"/><path d="m23.11,12.59c-0.65,0 -1.64,0.03 -1.95,0.07c-1.3,0.17 -2.25,0.55 -2.82,1.31c-0.3199,0.43 -0.48,0.99 -0.29,1.66c0.31,1.07 2.1501,1.44 3.4001,1.7c1.49,0.31 2.5599,0.57 3.5899,1.08c1.06,0.53 2.03,1.41 2.51,2.43c0.46,0.98 0.6001,1.96 0.5201,3.08c-0.11,1.56 -0.4701,2.72 -0.9901,3.69c-0.49,0.91 -1.32,1.87 -2.03,2.44c-0.74,0.59 -1.81,1.38 -3.15,1.83c-1.35,0.45 -3.1399,0.63 -5.2999,0.63h-12.30005c3.68,4.68 9.39005,7.68 15.80005,7.68c11.1,0 20.0899,-9 20.0899,-20.09c0,-2.65 -0.5199,-5.18 -1.4499,-7.5h-15.6201l-0.01,-0.01z" fill="#5B5B5B" id="svg_8"/></g></svg>
|
||||||
|
After Width: | Height: | Size: 1.5 KiB |
BIN
public/images/website_types/shopit/shopit_footer_logo.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
public/images/website_types/shopit/shopit_logo.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
163
tests/Feature/Address/AddressPresentationTest.php
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Feature\Address;
|
||||||
|
|
||||||
|
use App\Domains\Core\Address\Models\Address;
|
||||||
|
use App\Domains\Core\Tenant\Models\Tenant;
|
||||||
|
use App\Domains\Core\Tenant\Resources\TenantResource;
|
||||||
|
use App\Domains\Ticketing\Event\Models\Event;
|
||||||
|
use App\Domains\Ticketing\Event\Resources\EventResource;
|
||||||
|
use App\Domains\Ticketing\Event\Resources\PublicEventResource;
|
||||||
|
use App\Domains\Ticketing\Event\Services\EventAddressService;
|
||||||
|
use App\Domains\Ticketing\Event\Services\PublicEventService;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
class AddressPresentationTest extends TestCase
|
||||||
|
{
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
Schema::create('tenants', function (Blueprint $table): void {
|
||||||
|
$table->id();
|
||||||
|
$table->string('codigo')->unique();
|
||||||
|
});
|
||||||
|
Schema::create('events', function (Blueprint $table): void {
|
||||||
|
$table->id();
|
||||||
|
$table->string('tenant_code')->nullable();
|
||||||
|
$table->string('title')->nullable();
|
||||||
|
$table->string('subtitle')->nullable();
|
||||||
|
$table->timestamp('published_at')->nullable();
|
||||||
|
$table->foreignId('attachment_id')->nullable();
|
||||||
|
$table->string('location')->nullable();
|
||||||
|
$table->json('exact_location')->nullable();
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
(require database_path('migrations/2026_09_30_000000_create_addresses_and_tenant_addresses_tables.php'))->up();
|
||||||
|
(require database_path('migrations/2026_09_30_000200_add_address_id_to_events.php'))->up();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_tenant_response_exposes_all_addresses_main_address_and_a_separate_event_address(): void
|
||||||
|
{
|
||||||
|
DB::table('tenants')->insert(['codigo' => 'acme']);
|
||||||
|
$tenant = Tenant::where('codigo', 'acme')->firstOrFail();
|
||||||
|
$tenant->forceFill((new Tenant)->getAttributes());
|
||||||
|
$principal = Address::create(['label' => 'Principal', 'address_text' => 'Sede 123', 'latitude' => 0, 'longitude' => 0]);
|
||||||
|
$secondary = Address::create(['label' => 'Sucursal', 'address_text' => 'Otra calle 456']);
|
||||||
|
$venue = Address::create(['label' => 'Predio', 'address_text' => 'Evento 789', 'latitude' => -32.9431184, 'longitude' => -60.6437991]);
|
||||||
|
$tenant->addresses()->attach($principal->id, ['is_main' => true]);
|
||||||
|
$tenant->addresses()->attach($secondary->id);
|
||||||
|
$tenant->load(['addresses', 'mainAddress']);
|
||||||
|
foreach (['headerLogo', 'footerLogo', 'favicon', 'headerBackgroundImage', 'footerBackgroundImage', 'storefrontWebsiteType'] as $relation) {
|
||||||
|
$tenant->setRelation($relation, null);
|
||||||
|
}
|
||||||
|
$event = $this->presentedEvent($venue, $tenant);
|
||||||
|
$tenant->setRelation('activeEvent', $event);
|
||||||
|
$data = (new TenantResource($tenant))->response()->getData(true)['data'];
|
||||||
|
|
||||||
|
$this->assertCount(2, $data['addresses']);
|
||||||
|
$this->assertTrue($data['addresses'][0]['is_main']);
|
||||||
|
$this->assertFalse($data['addresses'][1]['is_main']);
|
||||||
|
$this->assertSame(0, $data['main_address']['latitude']);
|
||||||
|
$this->assertNull($data['addresses'][1]['latitude']);
|
||||||
|
$this->assertSame($venue->id, $data['event']['address']['id']);
|
||||||
|
$this->assertSame(-32.9431184, $data['event']['address']['latitude']);
|
||||||
|
$this->assertSame('Evento 789', $data['event']['address']['address_text']);
|
||||||
|
$this->assertSame('Anterior', $data['event']['location']);
|
||||||
|
$this->assertArrayNotHasKey('is_main', $data['event']['address']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_public_and_admin_event_responses_use_the_same_canonical_address(): void
|
||||||
|
{
|
||||||
|
$address = Address::create(['label' => 'Predio', 'address_text' => 'Nueva 123', 'latitude' => 0, 'longitude' => -60.6437991]);
|
||||||
|
$event = $this->presentedEvent($address, new Tenant);
|
||||||
|
foreach ([EventResource::class, PublicEventResource::class] as $resource) {
|
||||||
|
$data = (new $resource($event))->response()->getData(true)['data'];
|
||||||
|
$this->assertSame('Nueva 123', $data['address']['address_text']);
|
||||||
|
$this->assertSame(0, $data['address']['latitude']);
|
||||||
|
$this->assertSame(-60.6437991, $data['address']['longitude']);
|
||||||
|
$this->assertSame('Anterior', $data['location']);
|
||||||
|
$this->assertSame(20, $data['exact_location']['longitude']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$event->setRelation('address', null);
|
||||||
|
$data = (new PublicEventResource($event))->response()->getData(true)['data'];
|
||||||
|
$this->assertNull($data['address']);
|
||||||
|
$this->assertSame('Anterior', $data['location']);
|
||||||
|
$address->update(['latitude' => null, 'longitude' => null]);
|
||||||
|
$event->setRelation('address', $address);
|
||||||
|
$data = (new PublicEventResource($event))->response()->getData(true)['data'];
|
||||||
|
$this->assertNull($data['address']['latitude']);
|
||||||
|
$this->assertSame(20, $data['exact_location']['longitude']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_editing_event_location_preserves_canonical_coordinates_unless_explicitly_changed(): void
|
||||||
|
{
|
||||||
|
$address = Address::create(['label' => 'Predio', 'address_text' => 'Anterior', 'latitude' => 0, 'longitude' => -60]);
|
||||||
|
$event = Event::create(['address_id' => $address->id, 'location' => 'Nueva', 'exact_location' => ['latitude' => 10, 'longitude' => 20]]);
|
||||||
|
DB::transaction(fn () => app(EventAddressService::class)->syncLocation($event));
|
||||||
|
$this->assertSame('Nueva', $address->fresh()->address_text);
|
||||||
|
$this->assertSame('0.0000000', $address->fresh()->latitude);
|
||||||
|
$this->assertDatabaseCount('addresses', 1);
|
||||||
|
|
||||||
|
$event->exact_location = null;
|
||||||
|
DB::transaction(fn () => app(EventAddressService::class)->syncLocation($event, true));
|
||||||
|
$this->assertNull($address->fresh()->latitude);
|
||||||
|
$this->assertNull($address->fresh()->longitude);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_editing_a_shared_event_address_does_not_change_tenant_or_other_event_addresses(): void
|
||||||
|
{
|
||||||
|
DB::table('tenants')->insert(['codigo' => 'acme']);
|
||||||
|
$tenant = Tenant::where('codigo', 'acme')->firstOrFail();
|
||||||
|
$address = Address::create(['label' => 'Compartida', 'address_text' => 'Original', 'latitude' => -32, 'longitude' => -60]);
|
||||||
|
$tenant->addresses()->attach($address->id, ['is_main' => true]);
|
||||||
|
$event = Event::create(['address_id' => $address->id, 'location' => 'Nueva ubicación']);
|
||||||
|
$other = Event::create(['address_id' => $address->id, 'location' => 'Original']);
|
||||||
|
|
||||||
|
DB::transaction(fn () => app(EventAddressService::class)->syncLocation($event));
|
||||||
|
|
||||||
|
$this->assertSame('Original', $address->fresh()->address_text);
|
||||||
|
$this->assertSame($address->id, $other->fresh()->address_id);
|
||||||
|
$this->assertSame($address->id, $tenant->mainAddress->id);
|
||||||
|
$this->assertNotSame($address->id, $event->fresh()->address_id);
|
||||||
|
$this->assertSame('Nueva ubicación', $event->fresh()->address->address_text);
|
||||||
|
$this->assertSame('-32.0000000', $event->fresh()->address->latitude);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_public_event_search_uses_its_address_and_loads_it_for_the_response(): void
|
||||||
|
{
|
||||||
|
DB::table('tenants')->insert(['codigo' => 'acme']);
|
||||||
|
$tenant = Tenant::where('codigo', 'acme')->firstOrFail();
|
||||||
|
$address = Address::create(['label' => 'Predio', 'address_text' => 'Dirección nueva']);
|
||||||
|
$event = Event::create([
|
||||||
|
'tenant_code' => 'acme', 'title' => 'Festival', 'location' => 'Dirección anterior',
|
||||||
|
'address_id' => $address->id, 'published_at' => now()->subMinute(),
|
||||||
|
]);
|
||||||
|
$service = app(PublicEventService::class);
|
||||||
|
$page = $service->list($tenant, 'nueva', 1);
|
||||||
|
$this->assertSame($event->id, $page->items()[0]->id);
|
||||||
|
$this->assertTrue($page->items()[0]->relationLoaded('address'));
|
||||||
|
$this->assertSame(0, $service->list($tenant, 'anterior', 1)->total());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_editing_an_unlinked_event_creates_its_address(): void
|
||||||
|
{
|
||||||
|
$event = Event::create(['location' => 'Predio 123', 'exact_location' => ['latitude' => -32, 'longitude' => -60]]);
|
||||||
|
DB::transaction(fn () => app(EventAddressService::class)->syncLocation($event));
|
||||||
|
$this->assertSame('Predio 123', $event->fresh()->address->address_text);
|
||||||
|
$this->assertSame('-60.0000000', $event->fresh()->address->longitude);
|
||||||
|
$this->assertDatabaseCount('tenant_addresses', 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function presentedEvent(?Address $address, Tenant $tenant): Event
|
||||||
|
{
|
||||||
|
$event = new Event(['location' => 'Anterior', 'exact_location' => ['latitude' => 10, 'longitude' => 20]]);
|
||||||
|
$event->setRelation('address', $address)->setRelation('tenant', $tenant)
|
||||||
|
->setRelation('socialMedia', collect())->setRelation('dates', collect())->setRelation('attachment', null);
|
||||||
|
|
||||||
|
return $event;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,10 +2,13 @@
|
|||||||
|
|
||||||
namespace Tests\Feature\Auth;
|
namespace Tests\Feature\Auth;
|
||||||
|
|
||||||
|
use App\Domains\Core\Address\Models\Address;
|
||||||
use App\Domains\Core\Auth\Models\User;
|
use App\Domains\Core\Auth\Models\User;
|
||||||
use App\Domains\Core\Authorization\Enums\RoleCode;
|
use App\Domains\Core\Authorization\Enums\RoleCode;
|
||||||
use App\Domains\Core\Authorization\Models\Role;
|
use App\Domains\Core\Authorization\Models\Role;
|
||||||
use App\Domains\Core\Menu\Models\Menu;
|
use App\Domains\Core\Menu\Models\Menu;
|
||||||
|
use App\Domains\Core\PhoneNumber\Models\PhoneNumber;
|
||||||
|
use App\Domains\Core\Tenant\Models\SocialMedia;
|
||||||
use App\Domains\Core\Tenant\Models\Tenant;
|
use App\Domains\Core\Tenant\Models\Tenant;
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
use Laravel\Sanctum\Sanctum;
|
use Laravel\Sanctum\Sanctum;
|
||||||
@@ -46,6 +49,21 @@ class AdminAppMeControllerTest extends TestCase
|
|||||||
$tenant->menues()->sync([$catalog->code, $staff->code, $storefront->code]);
|
$tenant->menues()->sync([$catalog->code, $staff->code, $storefront->code]);
|
||||||
$otherTenant->menues()->sync([$staff->code]);
|
$otherTenant->menues()->sync([$staff->code]);
|
||||||
|
|
||||||
|
$instagram = SocialMedia::query()->create([
|
||||||
|
'code' => 'instagram', 'name' => 'Instagram', 'icon' => 'fa-brands fa-instagram',
|
||||||
|
]);
|
||||||
|
$tenant->socialMedia()->attach($instagram->code, ['url' => 'https://instagram.com/acme', 'orden' => 0]);
|
||||||
|
$address = Address::query()->create(['label' => 'Principal', 'address_text' => 'San Martín 123']);
|
||||||
|
$tenant->addresses()->attach($address->id, ['is_main' => true]);
|
||||||
|
$phone = PhoneNumber::query()->create([
|
||||||
|
'label' => 'Principal',
|
||||||
|
'country_code' => '+54',
|
||||||
|
'mobile_prefix' => '9',
|
||||||
|
'area_code' => '0341',
|
||||||
|
'local_number' => '6658247',
|
||||||
|
]);
|
||||||
|
$tenant->phoneNumbers()->attach($phone->id, ['is_main' => true]);
|
||||||
|
|
||||||
$user = User::factory()->create([
|
$user = User::factory()->create([
|
||||||
'nombre_apellido' => 'Admin Acme',
|
'nombre_apellido' => 'Admin Acme',
|
||||||
'email' => 'admin@acme.test',
|
'email' => 'admin@acme.test',
|
||||||
@@ -59,6 +77,11 @@ class AdminAppMeControllerTest extends TestCase
|
|||||||
->assertJsonPath('data.user.id', $user->id)
|
->assertJsonPath('data.user.id', $user->id)
|
||||||
->assertJsonPath('data.user.email', 'admin@acme.test')
|
->assertJsonPath('data.user.email', 'admin@acme.test')
|
||||||
->assertJsonPath('data.tenant.codigo', 'acme')
|
->assertJsonPath('data.tenant.codigo', 'acme')
|
||||||
|
->assertJsonPath('data.tenant.social_media.0.url', 'https://instagram.com/acme')
|
||||||
|
->assertJsonPath('data.tenant.addresses.0.address_text', 'San Martín 123')
|
||||||
|
->assertJsonPath('data.tenant.phone_numbers.0.number', '+54 9 (0341) 6658247')
|
||||||
|
->assertJsonPath('data.tenant.phone_numbers.0.area_code', '0341')
|
||||||
|
->assertJsonPath('data.tenant.phone_numbers.0.local_number', '6658247')
|
||||||
->assertJsonCount(1, 'data.tenant.menues')
|
->assertJsonCount(1, 'data.tenant.menues')
|
||||||
->assertJsonPath('data.tenant.menues.0.code', 'admin.catalog')
|
->assertJsonPath('data.tenant.menues.0.code', 'admin.catalog')
|
||||||
->assertJsonPath('data.tenant.menues.0.label', 'Catálogo')
|
->assertJsonPath('data.tenant.menues.0.label', 'Catálogo')
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ class ScannerMeControllerTest extends TestCase
|
|||||||
'route' => '/scanner/scan',
|
'route' => '/scanner/scan',
|
||||||
]);
|
]);
|
||||||
$foreign = Menu::query()->create([
|
$foreign = Menu::query()->create([
|
||||||
'code' => 'adminapp.inicio',
|
'code' => 'onticket.adminapp.inicio',
|
||||||
'label' => 'Administración',
|
'label' => 'Administración',
|
||||||
'route' => '/admin/inicio',
|
'route' => '/admin/inicio',
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -186,7 +186,7 @@ class EntryControllerTest extends TestCase
|
|||||||
'admin_website_type_code' => 'onticket',
|
'admin_website_type_code' => 'onticket',
|
||||||
]);
|
]);
|
||||||
$menu = Menu::query()->firstOrCreate(
|
$menu = Menu::query()->firstOrCreate(
|
||||||
['code' => 'adminapp.desfile.entradas'],
|
['code' => 'onticket.adminapp.desfile.entradas'],
|
||||||
['label' => 'Entradas', 'route' => '/admin/desfile/entradas'],
|
['label' => 'Entradas', 'route' => '/admin/desfile/entradas'],
|
||||||
);
|
);
|
||||||
$tenant->menues()->attach($menu->code);
|
$tenant->menues()->attach($menu->code);
|
||||||
|
|||||||
@@ -236,7 +236,7 @@ class AccommodationControllerTest extends TestCase
|
|||||||
|
|
||||||
if ($withMenu) {
|
if ($withMenu) {
|
||||||
$menu = Menu::query()->firstOrCreate(
|
$menu = Menu::query()->firstOrCreate(
|
||||||
['code' => 'adminapp.fiesta-futbol-infantil.alojamientos'],
|
['code' => 'onticket.adminapp.fiesta-futbol-infantil.alojamientos'],
|
||||||
['label' => 'Alojamientos', 'route' => '/admin/alojamientos'],
|
['label' => 'Alojamientos', 'route' => '/admin/alojamientos'],
|
||||||
);
|
);
|
||||||
$tenant->menues()->syncWithoutDetaching([$menu->code]);
|
$tenant->menues()->syncWithoutDetaching([$menu->code]);
|
||||||
|
|||||||
@@ -43,10 +43,10 @@ class EntryControllerTest extends TestCase
|
|||||||
{
|
{
|
||||||
$tenant = $this->createFiestaTenant();
|
$tenant = $this->createFiestaTenant();
|
||||||
$categories = [
|
$categories = [
|
||||||
'entries' => ['Entradas', 'adminapp.fiesta-futbol-infantil.entradas'],
|
'entries' => ['Entradas', 'onticket.adminapp.fiesta-futbol-infantil.entradas'],
|
||||||
'foods' => ['Comidas', 'adminapp.fiesta-futbol-infantil.comida'],
|
'foods' => ['Comidas', 'onticket.adminapp.fiesta-futbol-infantil.comida'],
|
||||||
'accommodations' => ['Alojamientos', 'adminapp.fiesta-futbol-infantil.alojamientos'],
|
'accommodations' => ['Alojamientos', 'onticket.adminapp.fiesta-futbol-infantil.alojamientos'],
|
||||||
'merchandise' => ['Merchandising', 'adminapp.fiesta-futbol-infantil.merchandising'],
|
'merchandise' => ['Merchandising', 'onticket.adminapp.fiesta-futbol-infantil.merchandising'],
|
||||||
];
|
];
|
||||||
|
|
||||||
foreach ($categories as [$label, $menuCode]) {
|
foreach ($categories as [$label, $menuCode]) {
|
||||||
@@ -405,7 +405,7 @@ class EntryControllerTest extends TestCase
|
|||||||
{
|
{
|
||||||
$tenant = $this->createTenant('fiesta_futbol_infantil');
|
$tenant = $this->createTenant('fiesta_futbol_infantil');
|
||||||
$menu = Menu::query()->create([
|
$menu = Menu::query()->create([
|
||||||
'code' => 'adminapp.fiesta-futbol-infantil.entradas',
|
'code' => 'onticket.adminapp.fiesta-futbol-infantil.entradas',
|
||||||
'label' => 'Entradas',
|
'label' => 'Entradas',
|
||||||
'route' => '/admin/entradas',
|
'route' => '/admin/entradas',
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -367,7 +367,7 @@ class FoodControllerTest extends TestCase
|
|||||||
'time_end' => '23:59',
|
'time_end' => '23:59',
|
||||||
]);
|
]);
|
||||||
$menu = Menu::query()->create([
|
$menu = Menu::query()->create([
|
||||||
'code' => 'adminapp.fiesta-futbol-infantil.comida',
|
'code' => 'onticket.adminapp.fiesta-futbol-infantil.comida',
|
||||||
'label' => 'Comida',
|
'label' => 'Comida',
|
||||||
'route' => '/admin/comidas',
|
'route' => '/admin/comidas',
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -293,7 +293,7 @@ class MerchandiseControllerTest extends TestCase
|
|||||||
['value' => 'XL', 'label' => 'XL', 'sort_order' => 4],
|
['value' => 'XL', 'label' => 'XL', 'sort_order' => 4],
|
||||||
]);
|
]);
|
||||||
$menu = Menu::query()->create([
|
$menu = Menu::query()->create([
|
||||||
'code' => 'adminapp.fiesta-futbol-infantil.merchandising',
|
'code' => 'onticket.adminapp.fiesta-futbol-infantil.merchandising',
|
||||||
'label' => 'Merchandising',
|
'label' => 'Merchandising',
|
||||||
'route' => '/admin/merchandising',
|
'route' => '/admin/merchandising',
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -135,7 +135,7 @@ class AdminAppDesfileEntryReservationFormControllerTest extends TestCase
|
|||||||
private function grantReservationsMenu(Tenant $tenant): void
|
private function grantReservationsMenu(Tenant $tenant): void
|
||||||
{
|
{
|
||||||
$menu = Menu::query()->create([
|
$menu = Menu::query()->create([
|
||||||
'code' => 'adminapp.desfile.reservas',
|
'code' => 'onticket.adminapp.desfile.reservas',
|
||||||
'label' => 'Reserva de Tickets',
|
'label' => 'Reserva de Tickets',
|
||||||
'route' => '/admin/desfile/reservas',
|
'route' => '/admin/desfile/reservas',
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -349,7 +349,7 @@ class AdminAppTicketFilterFormControllerTest extends TestCase
|
|||||||
private function grantTicketsMenu(Tenant $tenant): void
|
private function grantTicketsMenu(Tenant $tenant): void
|
||||||
{
|
{
|
||||||
$menu = Menu::query()->create([
|
$menu = Menu::query()->create([
|
||||||
'code' => 'adminapp.tickets',
|
'code' => 'onticket.adminapp.tickets',
|
||||||
'label' => 'Tickets',
|
'label' => 'Tickets',
|
||||||
'route' => '/admin/tickets',
|
'route' => '/admin/tickets',
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -2,20 +2,22 @@
|
|||||||
|
|
||||||
namespace Tests\Feature\Integration;
|
namespace Tests\Feature\Integration;
|
||||||
|
|
||||||
use App\Shared\Attachable\Enums\AttachmentType;
|
|
||||||
use App\Shared\Attachable\Models\Attachment;
|
|
||||||
use App\Domains\Core\Auth\Models\User;
|
|
||||||
use App\Domains\Commerce\Cart\Models\Cart;
|
use App\Domains\Commerce\Cart\Models\Cart;
|
||||||
use App\Domains\Commerce\Catalog\Enums\InventoryPolicy;
|
use App\Domains\Commerce\Catalog\Enums\InventoryPolicy;
|
||||||
use App\Domains\Commerce\Catalog\Models\CatalogItem;
|
use App\Domains\Commerce\Catalog\Models\CatalogItem;
|
||||||
use App\Domains\Commerce\Catalog\Models\Category;
|
use App\Domains\Commerce\Catalog\Models\Category;
|
||||||
use App\Domains\Commerce\Catalog\Models\Inventory;
|
use App\Domains\Commerce\Catalog\Models\Inventory;
|
||||||
use App\Domains\Commerce\Catalog\Models\Variant;
|
use App\Domains\Commerce\Catalog\Models\Variant;
|
||||||
use App\Shared\Integration\Models\Integration;
|
|
||||||
use App\Domains\Commerce\Purchase\Models\Purchase;
|
use App\Domains\Commerce\Purchase\Models\Purchase;
|
||||||
use App\Domains\Commerce\Purchase\Models\TelepagosPayment;
|
use App\Domains\Commerce\Purchase\Models\TelepagosPayment;
|
||||||
use App\Domains\Commerce\Purchase\Services\CheckoutService;
|
use App\Domains\Commerce\Purchase\Services\CheckoutService;
|
||||||
|
use App\Domains\Core\Auth\Models\User;
|
||||||
|
use App\Domains\Core\Client\Models\Client;
|
||||||
use App\Domains\Core\Tenant\Models\Tenant;
|
use App\Domains\Core\Tenant\Models\Tenant;
|
||||||
|
use App\Domains\Ticketing\Event\Models\Event;
|
||||||
|
use App\Shared\Attachable\Enums\AttachmentType;
|
||||||
|
use App\Shared\Attachable\Models\Attachment;
|
||||||
|
use App\Shared\Integration\Models\Integration;
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
use Illuminate\Support\Facades\Cache;
|
use Illuminate\Support\Facades\Cache;
|
||||||
use Illuminate\Support\Facades\Http;
|
use Illuminate\Support\Facades\Http;
|
||||||
@@ -227,6 +229,77 @@ class TelepagosWebhookTest extends TestCase
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function test_transfer_webhook_matches_the_client_configured_on_the_event(): void
|
||||||
|
{
|
||||||
|
$tenant = $this->createTenant('multi-event', 'Multi Event', 'multi-event.com.ar');
|
||||||
|
$eventClient = Client::query()->create([
|
||||||
|
'code' => 'event-client',
|
||||||
|
'name' => 'Event Client',
|
||||||
|
]);
|
||||||
|
Integration::query()->create([
|
||||||
|
'integration_code' => 'telepagos_homo',
|
||||||
|
'name' => 'Telepagos',
|
||||||
|
'url' => 'https://api.telepagos.com.ar',
|
||||||
|
'integration_data_schema' => [
|
||||||
|
'username' => 'required|string',
|
||||||
|
'password' => 'required|string',
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
$this->createClientIntegration([
|
||||||
|
'client_id' => $eventClient->id,
|
||||||
|
'integration_code' => 'telepagos_homo',
|
||||||
|
'integration_data' => ['username' => 'event-user', 'password' => 'event-password'],
|
||||||
|
]);
|
||||||
|
$event = Event::query()->create([
|
||||||
|
'client_id' => $eventClient->id,
|
||||||
|
'tenant_code' => $tenant->codigo,
|
||||||
|
'title' => 'Event with its own client',
|
||||||
|
]);
|
||||||
|
$tenant->update(['active_event_id' => $event->id]);
|
||||||
|
|
||||||
|
$user = User::factory()->create();
|
||||||
|
$variant = $this->createVariantForTenant($tenant->codigo, 10, '50.00');
|
||||||
|
$purchase = $this->createPendingTransferPurchase(
|
||||||
|
$tenant,
|
||||||
|
$user->id,
|
||||||
|
$variant->id,
|
||||||
|
1,
|
||||||
|
'12345678',
|
||||||
|
);
|
||||||
|
|
||||||
|
Http::fake([
|
||||||
|
'https://api.telepagos.com.ar/v2/auth/token' => Http::response([
|
||||||
|
'status' => 'ok',
|
||||||
|
'token' => 'event-client-token',
|
||||||
|
'expires_at' => now()->addHour()->toIso8601String(),
|
||||||
|
]),
|
||||||
|
'https://api.telepagos.com.ar/v2/payment/cashin/event-client-payment' => Http::response([
|
||||||
|
'status' => 'ok',
|
||||||
|
'data' => [
|
||||||
|
'amount' => 50,
|
||||||
|
'operation_id' => 1,
|
||||||
|
'transaction_id' => 'tx-event-client',
|
||||||
|
'buyer' => ['cuit' => '20123456789'],
|
||||||
|
],
|
||||||
|
]),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->postJson('/api/webhooks/telepagos/event-client', [
|
||||||
|
'id' => 'event-client-payment',
|
||||||
|
])->assertOk()->assertJsonPath('status', 'success');
|
||||||
|
|
||||||
|
$this->assertSame($event->id, $purchase->event_id);
|
||||||
|
$this->assertDatabaseHas('compras', [
|
||||||
|
'id' => $purchase->id,
|
||||||
|
'event_id' => $event->id,
|
||||||
|
'status' => Purchase::STATUS_PAID,
|
||||||
|
]);
|
||||||
|
$this->assertDatabaseHas('telepagos_payments', [
|
||||||
|
'compra_id' => $purchase->id,
|
||||||
|
'transaction_id' => 'tx-event-client',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
public function test_transfer_webhook_buys_unlimited_inventory_without_reducing_real_stock(): void
|
public function test_transfer_webhook_buys_unlimited_inventory_without_reducing_real_stock(): void
|
||||||
{
|
{
|
||||||
$tenant = $this->createTenant('unlimited', 'Unlimited', 'unlimited.com.ar');
|
$tenant = $this->createTenant('unlimited', 'Unlimited', 'unlimited.com.ar');
|
||||||
|
|||||||
57
tests/Feature/Migrations/AddClientIdToEventsTest.php
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Feature\Migrations;
|
||||||
|
|
||||||
|
use App\Domains\Core\Client\Models\Client;
|
||||||
|
use App\Domains\Ticketing\Event\Models\Event;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
class AddClientIdToEventsTest extends TestCase
|
||||||
|
{
|
||||||
|
public function test_event_client_overrides_the_tenant_client_when_configured(): void
|
||||||
|
{
|
||||||
|
Schema::create('clients', function (Blueprint $table): void {
|
||||||
|
$table->id();
|
||||||
|
$table->string('code');
|
||||||
|
$table->string('name');
|
||||||
|
});
|
||||||
|
Schema::create('tenants', function (Blueprint $table): void {
|
||||||
|
$table->id();
|
||||||
|
$table->foreignId('client_id');
|
||||||
|
$table->string('codigo');
|
||||||
|
});
|
||||||
|
Schema::create('events', function (Blueprint $table): void {
|
||||||
|
$table->id();
|
||||||
|
$table->string('tenant_code');
|
||||||
|
$table->string('title');
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
|
||||||
|
DB::table('clients')->insert([
|
||||||
|
['id' => 10, 'code' => 'tenant-client', 'name' => 'Tenant Client'],
|
||||||
|
['id' => 11, 'code' => 'event-client', 'name' => 'Event Client'],
|
||||||
|
]);
|
||||||
|
DB::table('tenants')->insert(['id' => 20, 'client_id' => 10, 'codigo' => 'tenant']);
|
||||||
|
DB::table('events')->insert([
|
||||||
|
'id' => 30,
|
||||||
|
'tenant_code' => 'tenant',
|
||||||
|
'title' => 'Event',
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$migration = require database_path('migrations/2026_09_28_000000_add_client_id_to_events.php');
|
||||||
|
$migration->up();
|
||||||
|
|
||||||
|
$event = Event::query()->findOrFail(30);
|
||||||
|
$this->assertSame(10, $event->effectiveClient()->id);
|
||||||
|
|
||||||
|
$event->update(['client_id' => 11]);
|
||||||
|
|
||||||
|
$this->assertSame(11, $event->fresh()->effectiveClient()->id);
|
||||||
|
$this->assertTrue(Client::query()->findOrFail(11)->events()->whereKey(30)->exists());
|
||||||
|
}
|
||||||
|
}
|
||||||
44
tests/Feature/Migrations/AddEventIdToPurchasesTest.php
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Feature\Migrations;
|
||||||
|
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
class AddEventIdToPurchasesTest extends TestCase
|
||||||
|
{
|
||||||
|
public function test_it_backfills_the_event_from_purchase_items(): void
|
||||||
|
{
|
||||||
|
Schema::create('events', fn (Blueprint $table) => $table->id());
|
||||||
|
Schema::create('compras', fn (Blueprint $table) => $table->id());
|
||||||
|
Schema::create('catalog_items', function (Blueprint $table): void {
|
||||||
|
$table->id();
|
||||||
|
$table->foreignId('event_id')->nullable();
|
||||||
|
});
|
||||||
|
Schema::create('compra_items', function (Blueprint $table): void {
|
||||||
|
$table->id();
|
||||||
|
$table->foreignId('compra_id');
|
||||||
|
$table->foreignId('source_catalog_item_id');
|
||||||
|
});
|
||||||
|
|
||||||
|
DB::table('events')->insert([['id' => 10], ['id' => 11]]);
|
||||||
|
DB::table('compras')->insert([['id' => 20], ['id' => 21]]);
|
||||||
|
DB::table('catalog_items')->insert([
|
||||||
|
['id' => 30, 'event_id' => 10],
|
||||||
|
['id' => 31, 'event_id' => 11],
|
||||||
|
]);
|
||||||
|
DB::table('compra_items')->insert([
|
||||||
|
['compra_id' => 20, 'source_catalog_item_id' => 30],
|
||||||
|
['compra_id' => 21, 'source_catalog_item_id' => 30],
|
||||||
|
['compra_id' => 21, 'source_catalog_item_id' => 31],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$migration = require database_path('migrations/2026_09_28_000100_add_event_id_to_compras.php');
|
||||||
|
$migration->up();
|
||||||
|
|
||||||
|
$this->assertDatabaseHas('compras', ['id' => 20, 'event_id' => 10]);
|
||||||
|
$this->assertDatabaseHas('compras', ['id' => 21, 'event_id' => null]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Feature\Migrations;
|
||||||
|
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
class AssignArrufoEventToPymeRuralClientTest extends TestCase
|
||||||
|
{
|
||||||
|
public function test_it_assigns_only_the_arrufo_event_to_pyme_rural(): void
|
||||||
|
{
|
||||||
|
Schema::create('clients', function (Blueprint $table): void {
|
||||||
|
$table->id();
|
||||||
|
$table->string('code');
|
||||||
|
});
|
||||||
|
Schema::create('events', function (Blueprint $table): void {
|
||||||
|
$table->id();
|
||||||
|
$table->foreignId('client_id')->nullable();
|
||||||
|
$table->string('tenant_code');
|
||||||
|
$table->string('title');
|
||||||
|
});
|
||||||
|
|
||||||
|
DB::table('clients')->insert([
|
||||||
|
['id' => 10, 'code' => 'pyme_rural'],
|
||||||
|
['id' => 11, 'code' => 'onticket'],
|
||||||
|
]);
|
||||||
|
DB::table('events')->insert([
|
||||||
|
[
|
||||||
|
'id' => 20,
|
||||||
|
'client_id' => null,
|
||||||
|
'tenant_code' => 'onticket',
|
||||||
|
'title' => '26.º Fiesta de la Tradición y 4.º Encuentro de Agrupaciones Gauchas',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'id' => 21,
|
||||||
|
'client_id' => 11,
|
||||||
|
'tenant_code' => 'onticket',
|
||||||
|
'title' => 'Otro evento',
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$migration = require database_path('migrations/2026_09_28_000200_assign_arrufo_event_to_pyme_rural_client.php');
|
||||||
|
$migration->up();
|
||||||
|
|
||||||
|
$this->assertDatabaseHas('events', ['id' => 20, 'client_id' => 10]);
|
||||||
|
$this->assertDatabaseHas('events', ['id' => 21, 'client_id' => 11]);
|
||||||
|
}
|
||||||
|
}
|
||||||
229
tests/Feature/Migrations/ContactAddressesTest.php
Normal file
@@ -0,0 +1,229 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Feature\Migrations;
|
||||||
|
|
||||||
|
use App\Domains\Core\Address\Models\Address;
|
||||||
|
use App\Domains\Core\Address\Models\TenantAddress;
|
||||||
|
use App\Domains\Core\Tenant\Models\Tenant;
|
||||||
|
use App\Domains\Core\Tenant\Services\TenantAddressService;
|
||||||
|
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\QueryException;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
class ContactAddressesTest extends TestCase
|
||||||
|
{
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
|
||||||
|
Schema::create('tenants', function (Blueprint $table): void {
|
||||||
|
$table->id();
|
||||||
|
$table->string('codigo')->unique();
|
||||||
|
$table->string('address')->nullable();
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
Schema::create('tenants_menues', function (Blueprint $table): void {
|
||||||
|
$table->id();
|
||||||
|
$table->string('tenant_code');
|
||||||
|
$table->string('menu_code');
|
||||||
|
$table->json('static_content')->nullable();
|
||||||
|
$table->boolean('is_enabled')->default(true);
|
||||||
|
});
|
||||||
|
DB::table('tenants')->insert([
|
||||||
|
['codigo' => 'alpha', 'address' => 'Dirección anterior'],
|
||||||
|
['codigo' => 'beta', 'address' => null],
|
||||||
|
['codigo' => 'empty', 'address' => null],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->schemaMigration()->up();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_it_copies_all_contact_locations_without_changing_legacy_data_and_can_run_again(): void
|
||||||
|
{
|
||||||
|
$sharedAddress = [
|
||||||
|
'label' => 'Sede principal',
|
||||||
|
'address' => 'San Lorenzo 1543, Rosario',
|
||||||
|
'coordinates' => [-32.9431184, -60.6437991],
|
||||||
|
];
|
||||||
|
$this->contact('alpha', [
|
||||||
|
'rosario' => ['label' => 'Rosario', 'addresses' => [$sharedAddress]],
|
||||||
|
'santa_fe' => ['label' => 'Santa Fe', 'addresses' => [
|
||||||
|
['label' => 'Sucursal', 'address' => 'San Martín 100', 'coordinates' => [0, 0]],
|
||||||
|
['label' => 'Sin mapa', 'address' => 'San Martín 200'],
|
||||||
|
]],
|
||||||
|
], enabled: false);
|
||||||
|
$this->contact('beta', ['rosario' => ['addresses' => [$sharedAddress]]]);
|
||||||
|
$this->contact('empty', []);
|
||||||
|
DB::table('tenants_menues')->insert([
|
||||||
|
'tenant_code' => 'empty',
|
||||||
|
'menu_code' => 'help.faq',
|
||||||
|
'static_content' => json_encode(['locations' => [['addresses' => [$sharedAddress]]]]),
|
||||||
|
]);
|
||||||
|
$oldMenus = DB::table('tenants_menues')->orderBy('id')->get()->toArray();
|
||||||
|
|
||||||
|
$migration = $this->dataMigration();
|
||||||
|
$migration->up();
|
||||||
|
$migration->up();
|
||||||
|
|
||||||
|
$this->assertDatabaseCount('addresses', 4);
|
||||||
|
$this->assertDatabaseCount('tenant_addresses', 4);
|
||||||
|
$this->assertDatabaseHas('addresses', [
|
||||||
|
'label' => 'Sede principal',
|
||||||
|
'address_text' => 'San Lorenzo 1543, Rosario',
|
||||||
|
'latitude' => -32.9431184,
|
||||||
|
'longitude' => -60.6437991,
|
||||||
|
]);
|
||||||
|
$this->assertDatabaseHas('addresses', ['label' => 'Sucursal', 'latitude' => 0, 'longitude' => 0]);
|
||||||
|
$this->assertDatabaseHas('addresses', ['label' => 'Sin mapa', 'latitude' => null, 'longitude' => null]);
|
||||||
|
$this->assertEquals($oldMenus, DB::table('tenants_menues')->orderBy('id')->get()->toArray());
|
||||||
|
$this->assertDatabaseHas('tenants', ['codigo' => 'alpha', 'address' => 'Dirección anterior']);
|
||||||
|
|
||||||
|
$alpha = Tenant::query()->where('codigo', 'alpha')->firstOrFail();
|
||||||
|
$beta = Tenant::query()->where('codigo', 'beta')->firstOrFail();
|
||||||
|
$this->assertCount(3, $alpha->addresses);
|
||||||
|
$this->assertTrue($alpha->addresses->first()->pivot->is_main);
|
||||||
|
$this->assertInstanceOf(TenantAddress::class, $alpha->addresses->first()->pivot);
|
||||||
|
$this->assertSame('Sede principal', $alpha->mainAddress->label);
|
||||||
|
$this->assertSame('-32.9431184', $alpha->mainAddress->latitude);
|
||||||
|
$this->assertNotSame($alpha->mainAddress->id, $beta->mainAddress->id);
|
||||||
|
$this->assertSame('alpha', $alpha->mainAddress->tenants->sole()->codigo);
|
||||||
|
$this->assertNull(Tenant::query()->where('codigo', 'empty')->firstOrFail()->mainAddress);
|
||||||
|
$this->assertSame(2, DB::table('tenant_addresses')->where('is_main', true)->count());
|
||||||
|
|
||||||
|
$migration->down();
|
||||||
|
$this->assertDatabaseCount('addresses', 4);
|
||||||
|
$this->assertEquals($oldMenus, DB::table('tenants_menues')->orderBy('id')->get()->toArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_a_tenant_can_have_many_secondary_addresses_but_only_one_main(): void
|
||||||
|
{
|
||||||
|
$tenant = Tenant::query()->where('codigo', 'alpha')->firstOrFail();
|
||||||
|
$addresses = $this->addresses(3);
|
||||||
|
$tenant->addresses()->attach($addresses[0], ['is_main' => true]);
|
||||||
|
$tenant->addresses()->attach([$addresses[1], $addresses[2]], ['is_main' => false]);
|
||||||
|
$this->assertCount(3, $tenant->addresses);
|
||||||
|
|
||||||
|
$this->expectException(QueryException::class);
|
||||||
|
$tenant->addresses()->updateExistingPivot($addresses[1], ['is_main' => true]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_it_prevents_duplicate_links(): void
|
||||||
|
{
|
||||||
|
$tenant = Tenant::query()->where('codigo', 'alpha')->firstOrFail();
|
||||||
|
$address = $this->addresses(1)[0];
|
||||||
|
$tenant->addresses()->attach($address);
|
||||||
|
|
||||||
|
$this->expectException(QueryException::class);
|
||||||
|
$tenant->addresses()->attach($address);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_main_address_is_specific_to_each_tenant_and_can_be_switched(): void
|
||||||
|
{
|
||||||
|
$alpha = Tenant::query()->where('codigo', 'alpha')->firstOrFail();
|
||||||
|
$beta = Tenant::query()->where('codigo', 'beta')->firstOrFail();
|
||||||
|
$addresses = $this->addresses(2);
|
||||||
|
$alpha->addresses()->attach($addresses[0], ['is_main' => true]);
|
||||||
|
$alpha->addresses()->attach($addresses[1]);
|
||||||
|
$beta->addresses()->attach($addresses[0], ['is_main' => true]);
|
||||||
|
$this->assertSame($addresses[0], $alpha->mainAddress->id);
|
||||||
|
|
||||||
|
app(TenantAddressService::class)->setMainAddress($alpha, Address::findOrFail($addresses[1]));
|
||||||
|
|
||||||
|
$this->assertSame($addresses[1], $alpha->mainAddress->id);
|
||||||
|
$this->assertSame($addresses[0], $beta->mainAddress->id);
|
||||||
|
$this->assertSame(2, DB::table('tenant_addresses')->where('is_main', true)->count());
|
||||||
|
$loaded = Tenant::query()->with('mainAddress')->whereIn('codigo', ['alpha', 'beta'])->get()->keyBy('codigo');
|
||||||
|
$this->assertSame($addresses[1], $loaded['alpha']->mainAddress->id);
|
||||||
|
$this->assertSame($addresses[0], $loaded['beta']->mainAddress->id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_switching_to_an_unlinked_address_preserves_the_previous_main(): void
|
||||||
|
{
|
||||||
|
$tenant = Tenant::query()->where('codigo', 'alpha')->firstOrFail();
|
||||||
|
$addresses = $this->addresses(2);
|
||||||
|
$tenant->addresses()->attach($addresses[0], ['is_main' => true]);
|
||||||
|
|
||||||
|
try {
|
||||||
|
app(TenantAddressService::class)->setMainAddress($tenant, Address::findOrFail($addresses[1]));
|
||||||
|
$this->fail('An unlinked address must be rejected.');
|
||||||
|
} catch (ModelNotFoundException) {
|
||||||
|
$this->assertSame($addresses[0], $tenant->fresh()->mainAddress->id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_invalid_coordinates_roll_back_all_copies_and_preserve_source_data(): void
|
||||||
|
{
|
||||||
|
$this->contact('alpha', [['addresses' => [
|
||||||
|
['label' => 'Válida', 'address' => 'Primera 100', 'coordinates' => [-32, -60]],
|
||||||
|
['label' => 'Inválida', 'address' => 'Segunda 200', 'coordinates' => [-91, -60]],
|
||||||
|
]]]);
|
||||||
|
$oldContent = DB::table('tenants_menues')->value('static_content');
|
||||||
|
|
||||||
|
try {
|
||||||
|
$this->dataMigration()->up();
|
||||||
|
$this->fail('Invalid latitude must not be silently imported.');
|
||||||
|
} catch (\RuntimeException $exception) {
|
||||||
|
$this->assertStringContainsString('alpha', $exception->getMessage());
|
||||||
|
$this->assertDatabaseCount('addresses', 0);
|
||||||
|
$this->assertDatabaseCount('tenant_addresses', 0);
|
||||||
|
$this->assertSame($oldContent, DB::table('tenants_menues')->value('static_content'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_deleting_a_tenant_removes_links_but_keeps_shared_addresses(): void
|
||||||
|
{
|
||||||
|
$address = $this->addresses(1)[0];
|
||||||
|
foreach (['alpha', 'beta'] as $code) {
|
||||||
|
Tenant::query()->where('codigo', $code)->firstOrFail()->addresses()->attach($address, ['is_main' => true]);
|
||||||
|
}
|
||||||
|
Tenant::query()->where('codigo', 'alpha')->delete();
|
||||||
|
$this->assertDatabaseCount('tenant_addresses', 1);
|
||||||
|
$this->assertDatabaseCount('addresses', 1);
|
||||||
|
|
||||||
|
Address::findOrFail($address)->delete();
|
||||||
|
$this->assertDatabaseCount('tenant_addresses', 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_schema_can_be_rolled_back_without_removing_legacy_tables(): void
|
||||||
|
{
|
||||||
|
$this->schemaMigration()->down();
|
||||||
|
$this->assertFalse(Schema::hasTable('addresses'));
|
||||||
|
$this->assertFalse(Schema::hasTable('tenant_addresses'));
|
||||||
|
$this->assertTrue(Schema::hasTable('tenants_menues'));
|
||||||
|
$this->assertTrue(Schema::hasColumn('tenants', 'address'));
|
||||||
|
}
|
||||||
|
|
||||||
|
private function contact(string $code, array $locations, bool $enabled = true): void
|
||||||
|
{
|
||||||
|
DB::table('tenants_menues')->insert([
|
||||||
|
'tenant_code' => $code,
|
||||||
|
'menu_code' => 'help.contact',
|
||||||
|
'static_content' => json_encode(['phone' => '+54 123', 'locations' => $locations], JSON_THROW_ON_ERROR),
|
||||||
|
'is_enabled' => $enabled,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function addresses(int $count): array
|
||||||
|
{
|
||||||
|
$ids = [];
|
||||||
|
for ($i = 0; $i < $count; $i++) {
|
||||||
|
$ids[] = Address::create(['label' => "Sucursal {$i}", 'address_text' => "Calle {$i}"])->id;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $ids;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function schemaMigration(): Migration
|
||||||
|
{
|
||||||
|
return require database_path('migrations/2026_09_30_000000_create_addresses_and_tenant_addresses_tables.php');
|
||||||
|
}
|
||||||
|
|
||||||
|
private function dataMigration(): Migration
|
||||||
|
{
|
||||||
|
return require database_path('migrations/2026_09_30_000100_copy_contact_menu_addresses.php');
|
||||||
|
}
|
||||||
|
}
|
||||||
150
tests/Feature/Migrations/ContactPhoneNumbersTest.php
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Feature\Migrations;
|
||||||
|
|
||||||
|
use App\Domains\Core\PhoneNumber\Models\PhoneNumber;
|
||||||
|
use App\Domains\Core\PhoneNumber\Resources\PhoneNumberResource;
|
||||||
|
use App\Domains\Core\Tenant\Models\Tenant;
|
||||||
|
use Illuminate\Database\QueryException;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
class ContactPhoneNumbersTest extends TestCase
|
||||||
|
{
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
Schema::create('tenants', function (Blueprint $table): void {
|
||||||
|
$table->id();
|
||||||
|
$table->string('codigo')->unique();
|
||||||
|
$table->string('phone')->nullable();
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
Schema::create('tenants_menues', function (Blueprint $table): void {
|
||||||
|
$table->id();
|
||||||
|
$table->string('tenant_code');
|
||||||
|
$table->string('menu_code');
|
||||||
|
$table->json('static_content')->nullable();
|
||||||
|
});
|
||||||
|
(require database_path('migrations/2026_09_30_000400_create_phone_numbers_and_tenant_phone_numbers_tables.php'))->up();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_tenant_phone_wins_and_copy_is_idempotent_and_scoped(): void
|
||||||
|
{
|
||||||
|
DB::table('tenants')->insert([
|
||||||
|
['codigo' => 'a', 'phone' => '+54 341 111-1111'],
|
||||||
|
['codigo' => 'b', 'phone' => '+54 341 111-1111'],
|
||||||
|
['codigo' => 'c', 'phone' => null],
|
||||||
|
['codigo' => 'empty', 'phone' => ''],
|
||||||
|
]);
|
||||||
|
$this->contact('a', '+54 (341) 222-2222');
|
||||||
|
$this->contact('b', '54 (341) 1111111');
|
||||||
|
$this->contact('c', '+54 341 3333333');
|
||||||
|
$copy = require database_path('migrations/2026_09_30_000500_copy_tenant_and_contact_phone_numbers.php');
|
||||||
|
$copy->up();
|
||||||
|
$copy->up();
|
||||||
|
$this->assertDatabaseCount('phone_numbers', 4);
|
||||||
|
$a = Tenant::where('codigo', 'a')->firstOrFail();
|
||||||
|
$b = Tenant::where('codigo', 'b')->firstOrFail();
|
||||||
|
$this->assertSame('+54 341 111-1111', $a->mainPhoneNumber->number);
|
||||||
|
$this->assertCount(2, $a->phoneNumbers);
|
||||||
|
$this->assertCount(1, $b->phoneNumbers);
|
||||||
|
$this->assertNotSame($a->mainPhoneNumber->id, $b->mainPhoneNumber->id);
|
||||||
|
$this->assertSame('+54 341 3333333', Tenant::where('codigo', 'c')->firstOrFail()->mainPhoneNumber->number);
|
||||||
|
$this->assertNull(Tenant::where('codigo', 'empty')->firstOrFail()->mainPhoneNumber);
|
||||||
|
$this->assertSame('tel:+543411111111', $a->mainPhoneNumber->telUrl());
|
||||||
|
$mainResource = PhoneNumberResource::make($a->mainPhoneNumber)->resolve();
|
||||||
|
$this->assertSame('+54 341 111-1111', $mainResource['number']);
|
||||||
|
$this->assertSame('tel:+543411111111', $mainResource['tel_url']);
|
||||||
|
$resources = PhoneNumberResource::collection($a->phoneNumbers)->resolve();
|
||||||
|
$this->assertTrue($resources[0]['is_main']);
|
||||||
|
$this->assertFalse($resources[1]['is_main']);
|
||||||
|
$copy->down();
|
||||||
|
$this->assertDatabaseCount('phone_numbers', 4);
|
||||||
|
$this->assertDatabaseHas('tenants', ['codigo' => 'a', 'phone' => '+54 341 111-1111']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_database_rejects_two_main_numbers(): void
|
||||||
|
{
|
||||||
|
DB::table('tenants')->insert(['codigo' => 'a', 'phone' => '+54 341 1111111']);
|
||||||
|
$this->contact('a', '+54 341 2222222');
|
||||||
|
(require database_path('migrations/2026_09_30_000500_copy_tenant_and_contact_phone_numbers.php'))->up();
|
||||||
|
$this->expectException(QueryException::class);
|
||||||
|
DB::table('tenant_phone_numbers')->where('tenant_codigo', 'a')->update(['is_main' => true]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_invalid_contact_rolls_back_the_whole_copy(): void
|
||||||
|
{
|
||||||
|
DB::table('tenants')->insert(['codigo' => 'a', 'phone' => '+54 341 1111111']);
|
||||||
|
$this->contact('a', ['invalid']);
|
||||||
|
try {
|
||||||
|
(require database_path('migrations/2026_09_30_000500_copy_tenant_and_contact_phone_numbers.php'))->up();
|
||||||
|
$this->fail('Invalid legacy numbers must not be silently discarded.');
|
||||||
|
} catch (\RuntimeException $exception) {
|
||||||
|
$this->assertStringContainsString('Invalid phone number', $exception->getMessage());
|
||||||
|
$this->assertDatabaseCount('phone_numbers', 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_a_number_cannot_be_linked_to_a_missing_tenant(): void
|
||||||
|
{
|
||||||
|
$id = DB::table('phone_numbers')->insertGetId(['label' => 'Contacto', 'number' => '123456']);
|
||||||
|
$this->expectException(QueryException::class);
|
||||||
|
DB::table('tenant_phone_numbers')->insert([
|
||||||
|
'tenant_codigo' => 'missing', 'phone_number_id' => $id, 'is_main' => true,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_it_splits_mobile_and_landline_numbers_and_builds_dialable_urls(): void
|
||||||
|
{
|
||||||
|
DB::table('phone_numbers')->insert([
|
||||||
|
['label' => 'Móvil', 'number' => '+54 9 (0341) 6658247'],
|
||||||
|
['label' => 'Fijo', 'number' => '+54 341 111-1111'],
|
||||||
|
['label' => 'Local', 'number' => '0341 1234567'],
|
||||||
|
['label' => 'Móvil sin signo', 'number' => '54 9 (0341) 6658247'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$migration = require database_path('migrations/2026_09_30_000700_split_phone_number_components.php');
|
||||||
|
$migration->up();
|
||||||
|
|
||||||
|
[$mobile, $landline, $local, $mobileWithoutPlus] = PhoneNumber::query()->orderBy('id')->get()->all();
|
||||||
|
$this->assertSame('+54', $mobile->country_code);
|
||||||
|
$this->assertSame('9', $mobile->mobile_prefix);
|
||||||
|
$this->assertSame('0341', $mobile->area_code);
|
||||||
|
$this->assertSame('6658247', $mobile->local_number);
|
||||||
|
$this->assertSame('+54 9 (0341) 6658247', $mobile->number);
|
||||||
|
$this->assertSame('tel:+5493416658247', $mobile->telUrl());
|
||||||
|
$this->assertSame('+54 341 1111111', $landline->number);
|
||||||
|
$this->assertSame('tel:+543411111111', $landline->telUrl());
|
||||||
|
$this->assertSame('0341 1234567', $local->number);
|
||||||
|
$this->assertSame('tel:03411234567', $local->telUrl());
|
||||||
|
$this->assertSame('+54 9 (0341) 6658247', $mobileWithoutPlus->number);
|
||||||
|
$resource = PhoneNumberResource::make($mobile)->resolve();
|
||||||
|
$this->assertSame('9', $resource['mobile_prefix']);
|
||||||
|
$this->assertSame('0341', $resource['area_code']);
|
||||||
|
$this->assertSame('tel:+5493416658247', $resource['tel_url']);
|
||||||
|
|
||||||
|
$created = PhoneNumber::query()->create([
|
||||||
|
'label' => 'Nuevo',
|
||||||
|
'area_code' => '0341',
|
||||||
|
'local_number' => '7777777',
|
||||||
|
]);
|
||||||
|
$this->assertSame('+54', $created->fresh()->country_code);
|
||||||
|
$this->assertSame('9', $created->fresh()->mobile_prefix);
|
||||||
|
$this->assertSame('+54 9 (0341) 7777777', $created->number);
|
||||||
|
$this->assertSame('tel:+5493417777777', $created->telUrl());
|
||||||
|
|
||||||
|
$migration->down();
|
||||||
|
$this->assertDatabaseHas('phone_numbers', ['id' => $mobile->id, 'number' => '+54 9 0341 6658247']);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function contact(string $tenant, mixed $phone): void
|
||||||
|
{
|
||||||
|
DB::table('tenants_menues')->insert([
|
||||||
|
'tenant_code' => $tenant, 'menu_code' => 'help.contact',
|
||||||
|
'static_content' => json_encode(['phone' => $phone], JSON_THROW_ON_ERROR),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
48
tests/Feature/Migrations/DynamicContactMenuTest.php
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Feature\Migrations;
|
||||||
|
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
class DynamicContactMenuTest extends TestCase
|
||||||
|
{
|
||||||
|
public function test_it_converts_only_contact_and_preserves_legacy_data_for_rollback(): void
|
||||||
|
{
|
||||||
|
Schema::create('menues', function (Blueprint $table): void {
|
||||||
|
$table->string('code')->primary();
|
||||||
|
$table->string('content_type');
|
||||||
|
$table->json('static_content_schema')->nullable();
|
||||||
|
});
|
||||||
|
Schema::create('tenants_menues', function (Blueprint $table): void {
|
||||||
|
$table->string('tenant_code');
|
||||||
|
$table->string('menu_code');
|
||||||
|
$table->json('static_content')->nullable();
|
||||||
|
});
|
||||||
|
DB::table('menues')->insert([
|
||||||
|
['code' => 'help.contact', 'content_type' => 'static', 'static_content_schema' => '{}'],
|
||||||
|
['code' => 'help.faq', 'content_type' => 'static', 'static_content_schema' => '{}'],
|
||||||
|
]);
|
||||||
|
$legacy = json_encode(['whatsapp' => ['whatsapp_url' => 'https://wa.me/123']], JSON_THROW_ON_ERROR);
|
||||||
|
DB::table('tenants_menues')->insert([
|
||||||
|
'tenant_code' => 'test', 'menu_code' => 'help.contact', 'static_content' => $legacy,
|
||||||
|
]);
|
||||||
|
$migration = require database_path('migrations/2026_09_30_000600_make_contact_menu_dynamic.php');
|
||||||
|
$migration->up();
|
||||||
|
$migration->up();
|
||||||
|
|
||||||
|
$this->assertDatabaseHas('menues', [
|
||||||
|
'code' => 'help.contact', 'content_type' => 'dynamic', 'static_content_schema' => null,
|
||||||
|
]);
|
||||||
|
$this->assertDatabaseHas('menues', ['code' => 'help.faq', 'content_type' => 'static', 'static_content_schema' => '{}']);
|
||||||
|
$this->assertDatabaseHas('tenants_menues', ['menu_code' => 'help.contact', 'static_content' => $legacy]);
|
||||||
|
|
||||||
|
$migration->down();
|
||||||
|
$this->assertDatabaseHas('menues', ['code' => 'help.contact', 'content_type' => 'static']);
|
||||||
|
$schema = json_decode(DB::table('menues')->where('code', 'help.contact')->value('static_content_schema'), true, flags: JSON_THROW_ON_ERROR);
|
||||||
|
$this->assertSame('required|url', $schema['whatsapp.whatsapp_url']);
|
||||||
|
$this->assertDatabaseHas('tenants_menues', ['menu_code' => 'help.contact', 'static_content' => $legacy]);
|
||||||
|
}
|
||||||
|
}
|
||||||
203
tests/Feature/Migrations/EventAddressesTest.php
Normal file
@@ -0,0 +1,203 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Feature\Migrations;
|
||||||
|
|
||||||
|
use App\Domains\Core\Address\Models\Address;
|
||||||
|
use App\Domains\Ticketing\Event\Models\Event;
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\QueryException;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
use PHPUnit\Framework\Attributes\DataProvider;
|
||||||
|
use RuntimeException;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
class EventAddressesTest extends TestCase
|
||||||
|
{
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
|
||||||
|
Schema::create('tenants', function (Blueprint $table): void {
|
||||||
|
$table->id();
|
||||||
|
$table->string('codigo')->unique();
|
||||||
|
});
|
||||||
|
Schema::create('events', function (Blueprint $table): void {
|
||||||
|
$table->id();
|
||||||
|
$table->string('tenant_code');
|
||||||
|
$table->string('title');
|
||||||
|
$table->string('location')->nullable();
|
||||||
|
$table->json('exact_location')->nullable();
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
$addresses = require database_path('migrations/2026_09_30_000000_create_addresses_and_tenant_addresses_tables.php');
|
||||||
|
$addresses->up();
|
||||||
|
$this->schemaMigration()->up();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_it_copies_event_locations_and_coordinates_and_preserves_legacy_data(): void
|
||||||
|
{
|
||||||
|
$precise = $this->event('Predio de Doma', ['latitude' => -32.9431184, 'longitude' => -60.6437991]);
|
||||||
|
$noMap = $this->event('Salón Luz y Fuerza');
|
||||||
|
$zero = $this->event('Punto cero', ['latitude' => 0, 'longitude' => 0]);
|
||||||
|
$missing = $this->event(null);
|
||||||
|
$blank = $this->event(' ');
|
||||||
|
$before = DB::table('events')->orderBy('id')->get()->map(fn (object $event): array => [
|
||||||
|
'id' => $event->id,
|
||||||
|
'tenant_code' => $event->tenant_code,
|
||||||
|
'title' => $event->title,
|
||||||
|
'location' => $event->location,
|
||||||
|
'exact_location' => $event->exact_location,
|
||||||
|
'created_at' => $event->created_at,
|
||||||
|
'updated_at' => $event->updated_at,
|
||||||
|
])->all();
|
||||||
|
|
||||||
|
$migration = $this->dataMigration();
|
||||||
|
$migration->up();
|
||||||
|
$migration->up();
|
||||||
|
|
||||||
|
$this->assertDatabaseCount('addresses', 3);
|
||||||
|
$this->assertDatabaseCount('tenant_addresses', 0);
|
||||||
|
$event = Event::findOrFail($precise);
|
||||||
|
$this->assertSame('Ubicación del evento', $event->address->label);
|
||||||
|
$this->assertSame('Predio de Doma', $event->address->address_text);
|
||||||
|
$this->assertSame('-32.9431184', $event->address->latitude);
|
||||||
|
$this->assertSame('-60.6437991', $event->address->longitude);
|
||||||
|
$this->assertNull(Event::findOrFail($noMap)->address->latitude);
|
||||||
|
$this->assertNull(Event::findOrFail($noMap)->address->longitude);
|
||||||
|
$this->assertSame('0.0000000', Event::findOrFail($zero)->address->latitude);
|
||||||
|
$this->assertSame('0.0000000', Event::findOrFail($zero)->address->longitude);
|
||||||
|
$this->assertNull(Event::findOrFail($missing)->address);
|
||||||
|
$this->assertNull(Event::findOrFail($blank)->address_id);
|
||||||
|
foreach ($before as $original) {
|
||||||
|
$this->assertDatabaseHas('events', $original);
|
||||||
|
}
|
||||||
|
|
||||||
|
$migration->down();
|
||||||
|
$this->assertDatabaseCount('addresses', 3);
|
||||||
|
$this->assertSame($event->address_id, Event::findOrFail($precise)->address_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_it_keeps_existing_links_and_creates_independent_copies_for_unlinked_events(): void
|
||||||
|
{
|
||||||
|
$original = Address::create(['label' => 'Existente', 'address_text' => 'Lugar elegido']);
|
||||||
|
$linked = $this->event('Texto anterior');
|
||||||
|
DB::table('events')->where('id', $linked)->update(['address_id' => $original->id]);
|
||||||
|
$first = $this->event('Mismo predio');
|
||||||
|
$second = $this->event('Mismo predio');
|
||||||
|
|
||||||
|
$this->dataMigration()->up();
|
||||||
|
|
||||||
|
$this->assertDatabaseCount('addresses', 3);
|
||||||
|
$this->assertSame($original->id, Event::findOrFail($linked)->address_id);
|
||||||
|
$this->assertNotSame(Event::findOrFail($first)->address_id, Event::findOrFail($second)->address_id);
|
||||||
|
$this->assertSame('Lugar elegido', $original->fresh()->address_text);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_multiple_events_can_share_one_address_through_the_model_relations(): void
|
||||||
|
{
|
||||||
|
$address = Address::create(['label' => 'Teatro', 'address_text' => 'San Martín 123']);
|
||||||
|
$first = Event::findOrFail($this->event('Texto original'));
|
||||||
|
$first->address()->associate($address);
|
||||||
|
$first->save();
|
||||||
|
$second = Event::findOrFail($this->event('Otro evento'));
|
||||||
|
$second->fill(['address_id' => (string) $address->id])->save();
|
||||||
|
|
||||||
|
$this->assertSame($address->id, $first->fresh()->address->id);
|
||||||
|
$this->assertSame($address->id, $second->fresh()->address_id);
|
||||||
|
$this->assertSame([$first->id, $second->id], $address->events()->orderBy('id')->pluck('id')->all());
|
||||||
|
$loaded = Event::with('address')->orderBy('id')->get();
|
||||||
|
$this->assertSame($address->id, $loaded->last()->address->id);
|
||||||
|
|
||||||
|
$first->delete();
|
||||||
|
$this->assertDatabaseCount('addresses', 1);
|
||||||
|
$address->delete();
|
||||||
|
$this->assertDatabaseHas('events', ['id' => $second->id, 'address_id' => null, 'location' => 'Otro evento']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_the_foreign_key_rejects_an_unknown_address(): void
|
||||||
|
{
|
||||||
|
$event = Event::findOrFail($this->event('Predio'));
|
||||||
|
|
||||||
|
$this->expectException(QueryException::class);
|
||||||
|
$event->update(['address_id' => 9999]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[DataProvider('invalidCoordinates')]
|
||||||
|
public function test_invalid_coordinates_roll_back_all_copies(string $coordinates): void
|
||||||
|
{
|
||||||
|
$valid = $this->event('Primero', ['latitude' => -32, 'longitude' => -60]);
|
||||||
|
$invalid = $this->event('Segundo');
|
||||||
|
DB::table('events')->where('id', $invalid)->update(['exact_location' => $coordinates]);
|
||||||
|
|
||||||
|
try {
|
||||||
|
$this->dataMigration()->up();
|
||||||
|
$this->fail('Invalid coordinates must stop the migration.');
|
||||||
|
} catch (\JsonException|RuntimeException) {
|
||||||
|
$this->assertDatabaseCount('addresses', 0);
|
||||||
|
$this->assertNull(Event::findOrFail($valid)->address_id);
|
||||||
|
$this->assertNull(Event::findOrFail($invalid)->address_id);
|
||||||
|
$this->assertSame($coordinates, DB::table('events')->where('id', $invalid)->value('exact_location'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function invalidCoordinates(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'latitude out of range' => ['{"latitude":-91,"longitude":-60}'],
|
||||||
|
'longitude out of range' => ['{"latitude":-32,"longitude":181}'],
|
||||||
|
'missing longitude' => ['{"latitude":-32}'],
|
||||||
|
'nonnumeric latitude' => ['{"latitude":"unknown","longitude":-60}'],
|
||||||
|
'invalid JSON' => ['not-json'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_coordinates_without_address_text_stop_the_copy_instead_of_losing_data(): void
|
||||||
|
{
|
||||||
|
$id = $this->event(null, ['latitude' => -32, 'longitude' => -60]);
|
||||||
|
|
||||||
|
try {
|
||||||
|
$this->dataMigration()->up();
|
||||||
|
$this->fail('Coordinates without address text require correction.');
|
||||||
|
} catch (RuntimeException $exception) {
|
||||||
|
$this->assertStringContainsString("Missing address text for event {$id}", $exception->getMessage());
|
||||||
|
$this->assertDatabaseCount('addresses', 0);
|
||||||
|
$this->assertNull(Event::findOrFail($id)->address_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_schema_rollback_preserves_events_legacy_fields_and_copied_addresses(): void
|
||||||
|
{
|
||||||
|
$id = $this->event('Predio', ['latitude' => -32, 'longitude' => -60]);
|
||||||
|
$this->dataMigration()->up();
|
||||||
|
$this->schemaMigration()->down();
|
||||||
|
|
||||||
|
$this->assertFalse(Schema::hasColumn('events', 'address_id'));
|
||||||
|
$this->assertDatabaseHas('events', ['id' => $id, 'location' => 'Predio']);
|
||||||
|
$this->assertDatabaseCount('addresses', 1);
|
||||||
|
$this->assertTrue(Schema::hasColumn('events', 'exact_location'));
|
||||||
|
}
|
||||||
|
|
||||||
|
private function event(?string $location, ?array $coordinates = null): int
|
||||||
|
{
|
||||||
|
return DB::table('events')->insertGetId([
|
||||||
|
'tenant_code' => 'alpha',
|
||||||
|
'title' => 'Evento',
|
||||||
|
'location' => $location,
|
||||||
|
'exact_location' => $coordinates === null ? null : json_encode($coordinates, JSON_THROW_ON_ERROR),
|
||||||
|
'created_at' => '2026-09-01 12:00:00',
|
||||||
|
'updated_at' => '2026-09-02 12:00:00',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function schemaMigration(): Migration
|
||||||
|
{
|
||||||
|
return require database_path('migrations/2026_09_30_000200_add_address_id_to_events.php');
|
||||||
|
}
|
||||||
|
|
||||||
|
private function dataMigration(): Migration
|
||||||
|
{
|
||||||
|
return require database_path('migrations/2026_09_30_000300_copy_event_locations_to_addresses.php');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -113,12 +113,16 @@ class InventoryStockMovementMigrationTest extends TestCase
|
|||||||
public function test_migration_rejects_an_inventory_with_negative_available_stock(): void
|
public function test_migration_rejects_an_inventory_with_negative_available_stock(): void
|
||||||
{
|
{
|
||||||
DB::table('inventories')->insert([
|
DB::table('inventories')->insert([
|
||||||
|
'id' => 23,
|
||||||
'real_stock' => 1,
|
'real_stock' => 1,
|
||||||
'reserved_stock' => 2,
|
'reserved_stock' => 2,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->expectException(\RuntimeException::class);
|
$this->expectException(\RuntimeException::class);
|
||||||
$this->expectExceptionMessage('stock disponible actual es negativo');
|
$this->expectExceptionMessage(
|
||||||
|
'stock disponible actual es negativo. Inventarios detectados (máximo 20): '
|
||||||
|
.'id=23 [real=1, vendidas=0, reintegradas=0, reservadas=2, reservas_entradas=0, resultado=-1]'
|
||||||
|
);
|
||||||
|
|
||||||
$this->stockMigration()->up();
|
$this->stockMigration()->up();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,201 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Feature\Migrations;
|
||||||
|
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
class SeparateAdminAppMenusByWebsiteTypeTest extends TestCase
|
||||||
|
{
|
||||||
|
private const SHOPIT_MENU_CODES = [
|
||||||
|
'shopit.adminapp.marca',
|
||||||
|
'shopit.adminapp.contacto',
|
||||||
|
'shopit.adminapp.productos',
|
||||||
|
'shopit.adminapp.categorias',
|
||||||
|
'shopit.adminapp.transacciones',
|
||||||
|
'shopit.adminapp.reportes',
|
||||||
|
'shopit.adminapp.usuarios_roles',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
|
||||||
|
Schema::create('menues', function (Blueprint $table): void {
|
||||||
|
$table->id();
|
||||||
|
$table->string('code')->unique();
|
||||||
|
$table->string('label');
|
||||||
|
$table->string('parent_menu_code')->nullable();
|
||||||
|
$table->string('content_type')->default('dynamic');
|
||||||
|
$table->json('static_content_schema')->nullable();
|
||||||
|
$table->string('route');
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
Schema::create('tenants', function (Blueprint $table): void {
|
||||||
|
$table->string('codigo')->primary();
|
||||||
|
$table->string('admin_website_type_code')->nullable();
|
||||||
|
});
|
||||||
|
Schema::create('roles', function (Blueprint $table): void {
|
||||||
|
$table->string('codigo')->primary();
|
||||||
|
});
|
||||||
|
Schema::create('tenants_menues', function (Blueprint $table): void {
|
||||||
|
$table->id();
|
||||||
|
$table->string('tenant_code');
|
||||||
|
$table->string('menu_code');
|
||||||
|
$table->json('static_content')->nullable();
|
||||||
|
$table->timestamps();
|
||||||
|
$table->unique(['tenant_code', 'menu_code']);
|
||||||
|
});
|
||||||
|
Schema::create('roles_menues', function (Blueprint $table): void {
|
||||||
|
$table->id();
|
||||||
|
$table->string('rol_codigo');
|
||||||
|
$table->string('menu_codigo');
|
||||||
|
$table->unique(['rol_codigo', 'menu_codigo']);
|
||||||
|
});
|
||||||
|
|
||||||
|
$now = now();
|
||||||
|
DB::table('menues')->insert([
|
||||||
|
[
|
||||||
|
'code' => 'main.adminapp',
|
||||||
|
'label' => 'Administración',
|
||||||
|
'parent_menu_code' => null,
|
||||||
|
'route' => '/',
|
||||||
|
'created_at' => $now,
|
||||||
|
'updated_at' => $now,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'code' => 'adminapp.inicio',
|
||||||
|
'label' => 'Inicio',
|
||||||
|
'parent_menu_code' => 'main.adminapp',
|
||||||
|
'route' => '/admin/inicio',
|
||||||
|
'created_at' => $now,
|
||||||
|
'updated_at' => $now,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'code' => 'adminapp.tickets',
|
||||||
|
'label' => 'Tickets',
|
||||||
|
'parent_menu_code' => 'main.adminapp',
|
||||||
|
'route' => '/admin/tickets',
|
||||||
|
'created_at' => $now,
|
||||||
|
'updated_at' => $now,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'code' => 'account',
|
||||||
|
'label' => 'Mi cuenta',
|
||||||
|
'parent_menu_code' => null,
|
||||||
|
'route' => '/mi-cuenta',
|
||||||
|
'created_at' => $now,
|
||||||
|
'updated_at' => $now,
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
DB::table('tenants')->insert([
|
||||||
|
['codigo' => 'sonder', 'admin_website_type_code' => 'shopit'],
|
||||||
|
['codigo' => 'mutual_smep', 'admin_website_type_code' => 'shopit'],
|
||||||
|
['codigo' => 'fiesta_futbol_infantil', 'admin_website_type_code' => 'onticket'],
|
||||||
|
]);
|
||||||
|
DB::table('roles')->insert([
|
||||||
|
['codigo' => 'admin'],
|
||||||
|
['codigo' => 'adminapp'],
|
||||||
|
['codigo' => 'user'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
foreach (['sonder', 'mutual_smep', 'fiesta_futbol_infantil'] as $tenantCode) {
|
||||||
|
foreach (['main.adminapp', 'adminapp.inicio', 'account'] as $menuCode) {
|
||||||
|
DB::table('tenants_menues')->insert([
|
||||||
|
'tenant_code' => $tenantCode,
|
||||||
|
'menu_code' => $menuCode,
|
||||||
|
'created_at' => $now,
|
||||||
|
'updated_at' => $now,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DB::table('tenants_menues')->insert([
|
||||||
|
'tenant_code' => 'fiesta_futbol_infantil',
|
||||||
|
'menu_code' => 'adminapp.tickets',
|
||||||
|
'created_at' => $now,
|
||||||
|
'updated_at' => $now,
|
||||||
|
]);
|
||||||
|
DB::table('roles_menues')->insert([
|
||||||
|
['rol_codigo' => 'admin', 'menu_codigo' => 'adminapp.inicio'],
|
||||||
|
['rol_codigo' => 'adminapp', 'menu_codigo' => 'adminapp.inicio'],
|
||||||
|
['rol_codigo' => 'adminapp', 'menu_codigo' => 'adminapp.tickets'],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function tearDown(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('roles_menues');
|
||||||
|
Schema::dropIfExists('tenants_menues');
|
||||||
|
Schema::dropIfExists('roles');
|
||||||
|
Schema::dropIfExists('tenants');
|
||||||
|
Schema::dropIfExists('menues');
|
||||||
|
|
||||||
|
parent::tearDown();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_it_separates_admin_menus_by_admin_website_type_and_rolls_back(): void
|
||||||
|
{
|
||||||
|
$migration = require database_path(
|
||||||
|
'migrations/2026_09_29_000000_separate_adminapp_menus_by_website_type.php'
|
||||||
|
);
|
||||||
|
|
||||||
|
$migration->up();
|
||||||
|
$migration->up();
|
||||||
|
|
||||||
|
$this->assertDatabaseMissing('menues', ['code' => 'adminapp.inicio']);
|
||||||
|
$this->assertDatabaseHas('menues', ['code' => 'onticket.adminapp.inicio']);
|
||||||
|
$this->assertDatabaseHas('tenants_menues', [
|
||||||
|
'tenant_code' => 'fiesta_futbol_infantil',
|
||||||
|
'menu_code' => 'onticket.adminapp.tickets',
|
||||||
|
]);
|
||||||
|
$this->assertDatabaseHas('tenants_menues', [
|
||||||
|
'tenant_code' => 'fiesta_futbol_infantil',
|
||||||
|
'menu_code' => 'onticket.adminapp.inicio',
|
||||||
|
]);
|
||||||
|
|
||||||
|
foreach (['sonder', 'mutual_smep'] as $tenantCode) {
|
||||||
|
$adminMenuCodes = DB::table('tenants_menues')
|
||||||
|
->join('menues', 'menues.code', '=', 'tenants_menues.menu_code')
|
||||||
|
->where('tenants_menues.tenant_code', $tenantCode)
|
||||||
|
->where(fn ($query) => $query
|
||||||
|
->where('menues.code', 'main.adminapp')
|
||||||
|
->orWhere('menues.parent_menu_code', 'main.adminapp'))
|
||||||
|
->orderBy('menues.code')
|
||||||
|
->pluck('menues.code')
|
||||||
|
->all();
|
||||||
|
|
||||||
|
$this->assertSame(
|
||||||
|
collect(['main.adminapp', ...self::SHOPIT_MENU_CODES])->sort()->values()->all(),
|
||||||
|
$adminMenuCodes,
|
||||||
|
);
|
||||||
|
$this->assertDatabaseHas('tenants_menues', [
|
||||||
|
'tenant_code' => $tenantCode,
|
||||||
|
'menu_code' => 'account',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (self::SHOPIT_MENU_CODES as $menuCode) {
|
||||||
|
$this->assertDatabaseHas('roles_menues', [
|
||||||
|
'rol_codigo' => 'adminapp',
|
||||||
|
'menu_codigo' => $menuCode,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$migration->down();
|
||||||
|
|
||||||
|
$this->assertDatabaseHas('menues', ['code' => 'adminapp.inicio']);
|
||||||
|
$this->assertDatabaseMissing('menues', ['code' => 'onticket.adminapp.inicio']);
|
||||||
|
$this->assertDatabaseMissing('menues', ['code' => 'shopit.adminapp.marca']);
|
||||||
|
$this->assertDatabaseHas('tenants_menues', [
|
||||||
|
'tenant_code' => 'sonder',
|
||||||
|
'menu_code' => 'adminapp.inicio',
|
||||||
|
]);
|
||||||
|
$this->assertDatabaseHas('tenants_menues', [
|
||||||
|
'tenant_code' => 'fiesta_futbol_infantil',
|
||||||
|
'menu_code' => 'adminapp.tickets',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -34,7 +34,7 @@ class SetWebsiteTypeFaviconTest extends TestCase
|
|||||||
$this->assertSame(1, DB::table('attachments')->where('filename', 'onticket_favicon.svg')->count());
|
$this->assertSame(1, DB::table('attachments')->where('filename', 'onticket_favicon.svg')->count());
|
||||||
Storage::disk('s3')->assertExists($attachment->path);
|
Storage::disk('s3')->assertExists($attachment->path);
|
||||||
$this->assertSame(
|
$this->assertSame(
|
||||||
file_get_contents(public_path('images/website_types/onticket_favicon.svg')),
|
file_get_contents(public_path('images/website_types/onticket/onticket_favicon.svg')),
|
||||||
Storage::disk('s3')->get($attachment->path),
|
Storage::disk('s3')->get($attachment->path),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -135,7 +135,7 @@ class DesfilePuraTendenciaSeederTest extends TestCase
|
|||||||
|
|
||||||
$expectedMenus = DB::table('tenants_menues')
|
$expectedMenus = DB::table('tenants_menues')
|
||||||
->where('tenant_code', 'fiesta_futbol_infantil')
|
->where('tenant_code', 'fiesta_futbol_infantil')
|
||||||
->where('menu_code', 'not like', 'adminapp.fiesta-futbol-infantil.%')
|
->where('menu_code', 'not like', 'onticket.adminapp.fiesta-futbol-infantil.%')
|
||||||
->orderBy('menu_code')
|
->orderBy('menu_code')
|
||||||
->pluck('menu_code')
|
->pluck('menu_code')
|
||||||
->all();
|
->all();
|
||||||
@@ -147,9 +147,9 @@ class DesfilePuraTendenciaSeederTest extends TestCase
|
|||||||
|
|
||||||
$this->assertSame($expectedMenus, $actualMenus);
|
$this->assertSame($expectedMenus, $actualMenus);
|
||||||
$this->assertContains('main.adminapp', $actualMenus);
|
$this->assertContains('main.adminapp', $actualMenus);
|
||||||
$this->assertContains('adminapp.event', $actualMenus);
|
$this->assertContains('onticket.adminapp.event', $actualMenus);
|
||||||
$this->assertContains('scanner.scan', $actualMenus);
|
$this->assertContains('scanner.scan', $actualMenus);
|
||||||
$this->assertNotContains('adminapp.fiesta-futbol-infantil.entradas', $actualMenus);
|
$this->assertNotContains('onticket.adminapp.fiesta-futbol-infantil.entradas', $actualMenus);
|
||||||
|
|
||||||
$catalogItem = DB::table('catalog_items')
|
$catalogItem = DB::table('catalog_items')
|
||||||
->where('tenant_code', 'desfile_pura_tendencia')
|
->where('tenant_code', 'desfile_pura_tendencia')
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ namespace Tests\Feature\Seeders;
|
|||||||
use App\Domains\Core\Authorization\Enums\RoleCode;
|
use App\Domains\Core\Authorization\Enums\RoleCode;
|
||||||
use App\Domains\Core\Authorization\Models\Role;
|
use App\Domains\Core\Authorization\Models\Role;
|
||||||
use App\Domains\Core\Menu\Models\Menu;
|
use App\Domains\Core\Menu\Models\Menu;
|
||||||
|
use App\Domains\Core\Tenant\Models\AdminWebsiteType;
|
||||||
use App\Domains\Core\Tenant\Models\Tenant;
|
use App\Domains\Core\Tenant\Models\Tenant;
|
||||||
use App\Shared\Attachable\Enums\AttachmentType;
|
use App\Shared\Attachable\Enums\AttachmentType;
|
||||||
use App\Shared\Attachable\Models\Attachment;
|
use App\Shared\Attachable\Models\Attachment;
|
||||||
@@ -25,21 +26,26 @@ class MenuSeederTest extends TestCase
|
|||||||
$this->seed(MenuSeeder::class);
|
$this->seed(MenuSeeder::class);
|
||||||
|
|
||||||
$expectedMenus = [
|
$expectedMenus = [
|
||||||
'adminapp.inicio' => ['Inicio', '/admin/inicio'],
|
'onticket.adminapp.inicio' => ['Inicio', '/admin/inicio'],
|
||||||
'adminapp.catalog' => ['Catálogo', '/admin/catalog'],
|
'onticket.adminapp.catalog' => ['Catálogo', '/admin/catalog'],
|
||||||
'adminapp.categories' => ['Categorías', '/admin/categories'],
|
'onticket.adminapp.categories' => ['Categorías', '/admin/categories'],
|
||||||
'adminapp.combos' => ['Combos', '/admin/combos'],
|
'onticket.adminapp.combos' => ['Combos', '/admin/combos'],
|
||||||
'adminapp.event' => ['Eventos', '/admin/event'],
|
'onticket.adminapp.event' => ['Eventos', '/admin/event'],
|
||||||
'adminapp.staff' => ['Staff', '/admin/staff'],
|
'onticket.adminapp.staff' => ['Usuarios', '/admin/staff'],
|
||||||
'adminapp.ventas' => ['Ventas', '/admin/ventas'],
|
'onticket.adminapp.ventas' => ['Ventas', '/admin/ventas'],
|
||||||
];
|
];
|
||||||
$fiestaCategoryMenus = [
|
$fiestaCategoryMenus = [
|
||||||
'adminapp.tickets' => ['Tickets', '/admin/tickets'],
|
'onticket.adminapp.tickets' => ['Tickets', '/admin/tickets'],
|
||||||
'adminapp.fiesta-futbol-infantil.entradas' => ['Entradas', '/admin/entradas'],
|
'onticket.adminapp.fiesta-futbol-infantil.entradas' => ['Entradas', '/admin/entradas'],
|
||||||
'adminapp.fiesta-futbol-infantil.alojamientos' => ['Alojamientos', '/admin/alojamientos'],
|
'onticket.adminapp.fiesta-futbol-infantil.alojamientos' => ['Alojamientos', '/admin/alojamientos'],
|
||||||
'adminapp.fiesta-futbol-infantil.merchandising' => ['Merchandising', '/admin/merchandising'],
|
'onticket.adminapp.fiesta-futbol-infantil.merchandising' => ['Merchandising', '/admin/merchandising'],
|
||||||
'adminapp.fiesta-futbol-infantil.comida' => ['Comida', '/admin/comidas'],
|
'onticket.adminapp.fiesta-futbol-infantil.comida' => ['Comida', '/admin/comidas'],
|
||||||
];
|
];
|
||||||
|
$desfileMenus = [
|
||||||
|
'onticket.adminapp.desfile.entradas' => ['Entradas', '/admin/desfile/entradas'],
|
||||||
|
'onticket.adminapp.desfile.reservas' => ['Reserva de Tickets', '/admin/desfile/reservas'],
|
||||||
|
];
|
||||||
|
$shopItMenus = $this->shopItAdminMenus();
|
||||||
|
|
||||||
$adminApp = Menu::query()
|
$adminApp = Menu::query()
|
||||||
->with('children')
|
->with('children')
|
||||||
@@ -53,7 +59,8 @@ class MenuSeederTest extends TestCase
|
|||||||
collect(array_keys([
|
collect(array_keys([
|
||||||
...$expectedMenus,
|
...$expectedMenus,
|
||||||
...$fiestaCategoryMenus,
|
...$fiestaCategoryMenus,
|
||||||
'adminapp.desfile.entradas' => ['Entradas', '/admin/desfile/entradas'],
|
...$desfileMenus,
|
||||||
|
...$shopItMenus,
|
||||||
]))->sort()->values()->all(),
|
]))->sort()->values()->all(),
|
||||||
$adminApp->children->pluck('code')->sort()->values()->all()
|
$adminApp->children->pluck('code')->sort()->values()->all()
|
||||||
);
|
);
|
||||||
@@ -88,10 +95,10 @@ class MenuSeederTest extends TestCase
|
|||||||
}
|
}
|
||||||
|
|
||||||
foreach ([
|
foreach ([
|
||||||
'adminapp.inicio',
|
'onticket.adminapp.inicio',
|
||||||
'adminapp.catalog',
|
'onticket.adminapp.catalog',
|
||||||
'adminapp.categories',
|
'onticket.adminapp.categories',
|
||||||
'adminapp.combos',
|
'onticket.adminapp.combos',
|
||||||
] as $code) {
|
] as $code) {
|
||||||
$this->assertFalse(
|
$this->assertFalse(
|
||||||
$fiestaTenant->menues()->where('menues.code', $code)->exists()
|
$fiestaTenant->menues()->where('menues.code', $code)->exists()
|
||||||
@@ -116,6 +123,50 @@ class MenuSeederTest extends TestCase
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function test_it_assigns_only_shopit_admin_menus_to_shopit_admin_tenants(): void
|
||||||
|
{
|
||||||
|
AdminWebsiteType::query()->create([
|
||||||
|
'codigo' => 'shopit',
|
||||||
|
'nombre' => 'ShopIt',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$sonder = $this->createTenant('sonder', 'shopit');
|
||||||
|
$mutualSmep = $this->createTenant('mutual_smep', 'shopit');
|
||||||
|
$onTicket = $this->createTenant('onticket');
|
||||||
|
|
||||||
|
$this->seed(MenuSeeder::class);
|
||||||
|
|
||||||
|
$shopItMenuCodes = array_keys($this->shopItAdminMenus());
|
||||||
|
|
||||||
|
foreach ([$sonder, $mutualSmep] as $tenant) {
|
||||||
|
$adminMenuCodes = $tenant->menues()
|
||||||
|
->where(fn ($query) => $query
|
||||||
|
->where('menues.code', 'main.adminapp')
|
||||||
|
->orWhere('menues.parent_menu_code', 'main.adminapp'))
|
||||||
|
->orderBy('menues.code')
|
||||||
|
->pluck('menues.code')
|
||||||
|
->all();
|
||||||
|
|
||||||
|
$this->assertSame(
|
||||||
|
collect(['main.adminapp', ...$shopItMenuCodes])->sort()->values()->all(),
|
||||||
|
$adminMenuCodes,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($this->shopItAdminMenus() as $code => [$label, $route]) {
|
||||||
|
$menu = Menu::query()->where('code', $code)->firstOrFail();
|
||||||
|
|
||||||
|
$this->assertSame($label, $menu->label);
|
||||||
|
$this->assertSame($route, $menu->route);
|
||||||
|
$this->assertSame('main.adminapp', $menu->parent_menu_code);
|
||||||
|
$this->assertFalse($onTicket->menues()->where('menues.code', $code)->exists());
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->assertFalse(
|
||||||
|
Menu::query()->where('code', 'like', 'adminapp.%')->exists()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
public function test_it_assigns_menus_to_the_expected_roles(): void
|
public function test_it_assigns_menus_to_the_expected_roles(): void
|
||||||
{
|
{
|
||||||
$this->seed(AuthorizationSeeder::class);
|
$this->seed(AuthorizationSeeder::class);
|
||||||
@@ -162,7 +213,7 @@ class MenuSeederTest extends TestCase
|
|||||||
|
|
||||||
$this->assertSame($userMenuCodes, $userRoleMenuCodes);
|
$this->assertSame($userMenuCodes, $userRoleMenuCodes);
|
||||||
$this->assertNotContains('main.adminapp', $userRoleMenuCodes);
|
$this->assertNotContains('main.adminapp', $userRoleMenuCodes);
|
||||||
$this->assertNotContains('adminapp.catalog', $userRoleMenuCodes);
|
$this->assertNotContains('onticket.adminapp.catalog', $userRoleMenuCodes);
|
||||||
|
|
||||||
$scannerRoleMenuCodes = Role::query()
|
$scannerRoleMenuCodes = Role::query()
|
||||||
->where('codigo', RoleCode::Scanner->value)
|
->where('codigo', RoleCode::Scanner->value)
|
||||||
@@ -287,26 +338,8 @@ class MenuSeederTest extends TestCase
|
|||||||
], $faq->static_content_schema);
|
], $faq->static_content_schema);
|
||||||
|
|
||||||
$contact = Menu::query()->where('code', 'help.contact')->firstOrFail();
|
$contact = Menu::query()->where('code', 'help.contact')->firstOrFail();
|
||||||
$this->assertSame(Menu::CONTENT_TYPE_STATIC, $contact->content_type);
|
$this->assertSame(Menu::CONTENT_TYPE_DYNAMIC, $contact->content_type);
|
||||||
$this->assertSame('required|url', $contact->static_content_schema['whatsapp.whatsapp_url']);
|
$this->assertNull($contact->static_content_schema);
|
||||||
$this->assertSame('required|string', $contact->static_content_schema['phone']);
|
|
||||||
$this->assertSame(
|
|
||||||
'required|array|min:1',
|
|
||||||
$contact->static_content_schema['locations.*.addresses']
|
|
||||||
);
|
|
||||||
$this->assertSame(
|
|
||||||
'required|array|size:2',
|
|
||||||
$contact->static_content_schema['locations.*.addresses.*.coordinates']
|
|
||||||
);
|
|
||||||
$this->assertSame(
|
|
||||||
'required|string',
|
|
||||||
$contact->static_content_schema['locations.*.addresses.*.address']
|
|
||||||
);
|
|
||||||
$this->assertSame(
|
|
||||||
'required|numeric|between:-90,90',
|
|
||||||
$contact->static_content_schema['locations.*.addresses.*.coordinates.0']
|
|
||||||
);
|
|
||||||
$this->assertSame('prohibited', $contact->static_content_schema['map_locations']);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_it_assigns_help_to_the_supported_tenants_with_faq_content(): void
|
public function test_it_assigns_help_to_the_supported_tenants_with_faq_content(): void
|
||||||
@@ -334,30 +367,6 @@ class MenuSeederTest extends TestCase
|
|||||||
'is_active' => false,
|
'is_active' => false,
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
$expectedContact = [
|
|
||||||
'whatsapp' => [
|
|
||||||
'whatsapp_url' => 'https://wa.me/543412602222',
|
|
||||||
'whatsapp_label' => 'Chatea con nosotros',
|
|
||||||
],
|
|
||||||
'phone' => '+54 9 (0341) 6658247',
|
|
||||||
'locations' => [
|
|
||||||
'rosario' => [
|
|
||||||
'label' => 'Rosario',
|
|
||||||
'addresses' => [
|
|
||||||
[
|
|
||||||
'label' => 'Gigante de Arroyito',
|
|
||||||
'address' => 'Av. Génova 640, Rosario',
|
|
||||||
'coordinates' => [-32.913997, -60.674567],
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'label' => 'Telepagos',
|
|
||||||
'address' => 'Rioja 1150, piso 12, dpto. 3, Rosario',
|
|
||||||
'coordinates' => [-32.946820, -60.639320],
|
|
||||||
],
|
|
||||||
],
|
|
||||||
],
|
|
||||||
],
|
|
||||||
];
|
|
||||||
|
|
||||||
foreach ([$sonder, $fiesta] as $tenant) {
|
foreach ([$sonder, $fiesta] as $tenant) {
|
||||||
$helpMenus = $tenant->menues()
|
$helpMenus = $tenant->menues()
|
||||||
@@ -371,10 +380,7 @@ class MenuSeederTest extends TestCase
|
|||||||
$expectedQuestions,
|
$expectedQuestions,
|
||||||
$helpMenus->firstWhere('code', 'help.faq')->pivot->static_content
|
$helpMenus->firstWhere('code', 'help.faq')->pivot->static_content
|
||||||
);
|
);
|
||||||
$this->assertEquals(
|
$this->assertNull($helpMenus->firstWhere('code', 'help.contact')->pivot->static_content);
|
||||||
$expectedContact,
|
|
||||||
$helpMenus->firstWhere('code', 'help.contact')->pivot->static_content
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->assertFalse(
|
$this->assertFalse(
|
||||||
@@ -382,7 +388,7 @@ class MenuSeederTest extends TestCase
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function createTenant(string $code): Tenant
|
private function createTenant(string $code, ?string $adminWebsiteTypeCode = null): Tenant
|
||||||
{
|
{
|
||||||
$headerLogo = $this->createAttachment("{$code}-header.png");
|
$headerLogo = $this->createAttachment("{$code}-header.png");
|
||||||
$footerLogo = $this->createAttachment("{$code}-footer.png");
|
$footerLogo = $this->createAttachment("{$code}-footer.png");
|
||||||
@@ -391,6 +397,7 @@ class MenuSeederTest extends TestCase
|
|||||||
'codigo' => $code,
|
'codigo' => $code,
|
||||||
'nombre' => $code,
|
'nombre' => $code,
|
||||||
'dominio' => "{$code}.example.com",
|
'dominio' => "{$code}.example.com",
|
||||||
|
'admin_website_type_code' => $adminWebsiteTypeCode,
|
||||||
'primary_color' => '#111111',
|
'primary_color' => '#111111',
|
||||||
'secondary_color' => '#222222',
|
'secondary_color' => '#222222',
|
||||||
'danger_color' => '#333333',
|
'danger_color' => '#333333',
|
||||||
@@ -402,6 +409,20 @@ class MenuSeederTest extends TestCase
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @return array<string, array{string, string}> */
|
||||||
|
private function shopItAdminMenus(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'shopit.adminapp.marca' => ['Marca', '/admin/marca'],
|
||||||
|
'shopit.adminapp.contacto' => ['Contacto', '/admin/contacto'],
|
||||||
|
'shopit.adminapp.productos' => ['Productos', '/admin/productos'],
|
||||||
|
'shopit.adminapp.categorias' => ['Categorías', '/admin/categorias'],
|
||||||
|
'shopit.adminapp.transacciones' => ['Transacciones', '/admin/transacciones'],
|
||||||
|
'shopit.adminapp.reportes' => ['Reportes', '/admin/reportes'],
|
||||||
|
'shopit.adminapp.usuarios_roles' => ['Usuarios y roles', '/admin/usuarios-roles'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
private function createAttachment(string $filename): Attachment
|
private function createAttachment(string $filename): Attachment
|
||||||
{
|
{
|
||||||
return Attachment::query()->create([
|
return Attachment::query()->create([
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ class WebsiteTypeSeederTest extends TestCase
|
|||||||
|
|
||||||
$this->assertSame(2, AdminWebsiteType::query()->count());
|
$this->assertSame(2, AdminWebsiteType::query()->count());
|
||||||
$this->assertSame(3, StorefrontWebsiteType::query()->count());
|
$this->assertSame(3, StorefrontWebsiteType::query()->count());
|
||||||
$this->assertSame(5, Attachment::query()->count());
|
$this->assertSame(6, Attachment::query()->count());
|
||||||
|
|
||||||
$expectedPresentation = [
|
$expectedPresentation = [
|
||||||
'primary_color' => '#FF7006',
|
'primary_color' => '#FF7006',
|
||||||
@@ -45,12 +45,15 @@ class WebsiteTypeSeederTest extends TestCase
|
|||||||
$this->assertSame('ShopIt', $shopIt->nombre);
|
$this->assertSame('ShopIt', $shopIt->nombre);
|
||||||
$this->assertSame('localhost', $shopIt->dominio);
|
$this->assertSame('localhost', $shopIt->dominio);
|
||||||
$this->assertSame('scanner.localhost', $shopIt->scanner_domain);
|
$this->assertSame('scanner.localhost', $shopIt->scanner_domain);
|
||||||
$this->assertSame($expectedPresentation, $shopIt->only(array_keys($expectedPresentation)));
|
$this->assertSame(
|
||||||
$this->assertSame('onticket_logo.png', $shopIt->siteLogo->filename);
|
[...$expectedPresentation, 'primary_color' => '#2FD3AC'],
|
||||||
|
$shopIt->only(array_keys($expectedPresentation)),
|
||||||
|
);
|
||||||
|
$this->assertSame('shopit_logo.png', $shopIt->siteLogo->filename);
|
||||||
Storage::disk('s3')->assertExists($shopIt->siteLogo->path);
|
Storage::disk('s3')->assertExists($shopIt->siteLogo->path);
|
||||||
$this->assertSame('onticket_footer_logo.png', $shopIt->footerLogo->filename);
|
$this->assertSame('shopit_footer_logo.png', $shopIt->footerLogo->filename);
|
||||||
Storage::disk('s3')->assertExists($shopIt->footerLogo->path);
|
Storage::disk('s3')->assertExists($shopIt->footerLogo->path);
|
||||||
$this->assertSame('onticket_favicon.svg', $shopIt->favicon->filename);
|
$this->assertSame('shopit-favicon.svg', $shopIt->favicon->filename);
|
||||||
Storage::disk('s3')->assertExists($shopIt->favicon->path);
|
Storage::disk('s3')->assertExists($shopIt->favicon->path);
|
||||||
$shopItStorefront = StorefrontWebsiteType::query()->where('codigo', 'shopit')->with('extras')->sole();
|
$shopItStorefront = StorefrontWebsiteType::query()->where('codigo', 'shopit')->with('extras')->sole();
|
||||||
$this->assertSame('standard', $shopItStorefront->header_type);
|
$this->assertSame('standard', $shopItStorefront->header_type);
|
||||||
@@ -84,7 +87,7 @@ class WebsiteTypeSeederTest extends TestCase
|
|||||||
Storage::disk('s3')->assertExists($onTicket->footerLogo->path);
|
Storage::disk('s3')->assertExists($onTicket->footerLogo->path);
|
||||||
$this->assertNotSame($shopIt->site_logo, $onTicket->site_logo);
|
$this->assertNotSame($shopIt->site_logo, $onTicket->site_logo);
|
||||||
$this->assertNotSame($shopIt->footer_logo, $onTicket->footer_logo);
|
$this->assertNotSame($shopIt->footer_logo, $onTicket->footer_logo);
|
||||||
$this->assertSame($shopIt->favicon_id, $onTicket->favicon_id);
|
$this->assertNotSame($shopIt->favicon_id, $onTicket->favicon_id);
|
||||||
$this->assertSame('onticket_favicon.svg', $onTicket->favicon->filename);
|
$this->assertSame('onticket_favicon.svg', $onTicket->favicon->filename);
|
||||||
$onTicketStorefront = StorefrontWebsiteType::query()->where('codigo', 'onticket')->with('extras')->sole();
|
$onTicketStorefront = StorefrontWebsiteType::query()->where('codigo', 'onticket')->with('extras')->sole();
|
||||||
$this->assertSame('standard', $onTicketStorefront->header_type);
|
$this->assertSame('standard', $onTicketStorefront->header_type);
|
||||||
|
|||||||
263
tests/Feature/Tenant/AdminAppBrandControllerTest.php
Normal file
@@ -0,0 +1,263 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Feature\Tenant;
|
||||||
|
|
||||||
|
use App\Domains\Core\Auth\Models\User;
|
||||||
|
use App\Domains\Core\Authorization\Enums\RoleCode;
|
||||||
|
use App\Domains\Core\Tenant\Models\StorefrontWebsiteType;
|
||||||
|
use App\Domains\Core\Tenant\Models\Tenant;
|
||||||
|
use App\Shared\Attachable\Enums\AttachmentType;
|
||||||
|
use App\Shared\Attachable\Models\Attachment;
|
||||||
|
use App\Shared\Attachable\Models\AttachmentCrop;
|
||||||
|
use Database\Seeders\AuthorizationSeeder;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
use Illuminate\Http\UploadedFile;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
use Laravel\Sanctum\Sanctum;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
class AdminAppBrandControllerTest extends TestCase
|
||||||
|
{
|
||||||
|
use RefreshDatabase;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
|
||||||
|
$this->seed(AuthorizationSeeder::class);
|
||||||
|
Storage::fake('s3');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_only_the_authenticated_adminapp_tenant_brand_is_returned(): void
|
||||||
|
{
|
||||||
|
$websiteType = $this->websiteType('shopit');
|
||||||
|
$carousel = $websiteType->extras()->create([
|
||||||
|
'codigo' => 'carousel',
|
||||||
|
'nombre' => 'Carrusel principal',
|
||||||
|
'is_required' => false,
|
||||||
|
'config_schema' => [
|
||||||
|
'request_rules' => ['$' => 'required|array'],
|
||||||
|
'transforms' => ['$.*' => ['handler' => 'attachment', 'attachment_type' => 'image']],
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
$header = $this->image('header.png');
|
||||||
|
$footer = $this->image('footer.png');
|
||||||
|
$first = $this->image('first.png');
|
||||||
|
$second = $this->image('second.png');
|
||||||
|
$cropped = $this->image('first-cropped.png');
|
||||||
|
$first->cropVariants()->create([
|
||||||
|
'variant' => AttachmentCrop::DESKTOP,
|
||||||
|
'crop_horizontal' => ['start_percentage' => 10, 'end_percentage' => 90],
|
||||||
|
'crop_vertical' => ['start_percentage' => 20, 'end_percentage' => 80],
|
||||||
|
'cropped_attachment_id' => $cropped->id,
|
||||||
|
]);
|
||||||
|
$tenant = $this->tenant('acme', $websiteType, [
|
||||||
|
'nombre' => 'Acme',
|
||||||
|
'primary_color' => '#112233',
|
||||||
|
'header_bg_color' => '#223344',
|
||||||
|
'footer_bg_color' => '#112233',
|
||||||
|
'header_logo_id' => $header->id,
|
||||||
|
'footer_logo_id' => $footer->id,
|
||||||
|
]);
|
||||||
|
$tenant->websiteExtras()->create([
|
||||||
|
'website_type_extra_id' => $carousel->id,
|
||||||
|
'config' => [$first->id, $second->id],
|
||||||
|
]);
|
||||||
|
$other = $this->tenant('other', $websiteType, ['nombre' => 'Other']);
|
||||||
|
$other->websiteExtras()->create([
|
||||||
|
'website_type_extra_id' => $carousel->id,
|
||||||
|
'config' => [$this->image('other.png')->id],
|
||||||
|
]);
|
||||||
|
|
||||||
|
Sanctum::actingAs($this->admin($tenant));
|
||||||
|
|
||||||
|
$response = $this->getJson('/api/v1/adminapp/tenant/brand')
|
||||||
|
->assertOk()
|
||||||
|
->assertJsonPath('data.nombre', 'Acme')
|
||||||
|
->assertJsonPath('data.primary_color', '#112233')
|
||||||
|
->assertJsonPath('data.header_bg_color', '#223344')
|
||||||
|
->assertJsonPath('data.footer_color', 'primary')
|
||||||
|
->assertJsonCount(2, 'data.banner_images');
|
||||||
|
|
||||||
|
foreach (['header_logo' => $header, 'footer_logo' => $footer] as $field => $image) {
|
||||||
|
$this->assertStringContainsString($image->path, $response->json("data.{$field}"));
|
||||||
|
$response->assertJsonPath("data.{$field}_key", $image->key);
|
||||||
|
}
|
||||||
|
foreach ([$first, $second] as $index => $image) {
|
||||||
|
$response->assertJsonPath("data.banner_images.{$index}.key", $image->key);
|
||||||
|
$this->assertStringContainsString(
|
||||||
|
$image->path,
|
||||||
|
$response->json("data.banner_images.{$index}.original_url")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
$this->assertStringContainsString($cropped->path, $response->json('data.banner_images.0.url'));
|
||||||
|
$response->assertJsonPath('data.banner_images.0.crop.crop_horizontal.start_percentage', 10)
|
||||||
|
->assertJsonPath('data.banner_images.0.crop.crop_vertical.end_percentage', 80)
|
||||||
|
->assertJsonPath('data.banner_images.1.crop', null);
|
||||||
|
$this->assertStringNotContainsString('other.png', $response->getContent());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_update_keeps_original_carousel_attachment_and_returns_its_crop(): void
|
||||||
|
{
|
||||||
|
$websiteType = $this->websiteType('shopit');
|
||||||
|
$carousel = $websiteType->extras()->create([
|
||||||
|
'codigo' => 'carousel', 'nombre' => 'Carrusel', 'is_required' => false,
|
||||||
|
'config_schema' => ['request_rules' => [], 'transforms' => [
|
||||||
|
'$.*' => ['handler' => 'attachment', 'attachment_type' => 'image'],
|
||||||
|
]],
|
||||||
|
]);
|
||||||
|
$tenant = $this->tenant('acme', $websiteType, [
|
||||||
|
'primary_color' => '#112233', 'footer_bg_color' => '#112233',
|
||||||
|
]);
|
||||||
|
Sanctum::actingAs($this->admin($tenant));
|
||||||
|
$file = UploadedFile::fake()->image('banner.png', 400, 100);
|
||||||
|
$dataUrl = 'data:image/png;base64,'.base64_encode(file_get_contents($file->getRealPath()));
|
||||||
|
|
||||||
|
$response = $this->putJson('/api/v1/adminapp/tenant/brand', [
|
||||||
|
'nombre' => 'Acme Nueva', 'primary_color' => '#abcdef',
|
||||||
|
'footer_bg_color' => '#abcdef', 'header_logo' => null, 'footer_logo' => null,
|
||||||
|
'banner_images' => [[
|
||||||
|
'image' => $dataUrl,
|
||||||
|
'crop_horizontal' => ['start_percentage' => 10, 'end_percentage' => 90],
|
||||||
|
'crop_vertical' => ['start_percentage' => 0, 'end_percentage' => 100],
|
||||||
|
]],
|
||||||
|
])->assertOk()
|
||||||
|
->assertJsonPath('data.nombre', 'Acme Nueva')
|
||||||
|
->assertJsonPath('data.footer_color', 'primary')
|
||||||
|
->assertJsonPath('data.banner_images.0.crop.crop_horizontal.start_percentage', 10);
|
||||||
|
|
||||||
|
$original = Attachment::query()->where('key', $response->json('data.banner_images.0.key'))->firstOrFail();
|
||||||
|
$this->assertSame([$original->id], $tenant->websiteExtras()
|
||||||
|
->where('website_type_extra_id', $carousel->id)->firstOrFail()->config);
|
||||||
|
$this->assertStringContainsString($original->path, $response->json('data.banner_images.0.original_url'));
|
||||||
|
$this->assertNotSame($response->json('data.banner_images.0.original_url'),
|
||||||
|
$response->json('data.banner_images.0.url'));
|
||||||
|
|
||||||
|
$updated = $this->putJson('/api/v1/adminapp/tenant/brand', [
|
||||||
|
'nombre' => 'Acme Nueva', 'primary_color' => '#abcdef',
|
||||||
|
'footer_bg_color' => '#abcdef', 'header_logo' => null, 'footer_logo' => null,
|
||||||
|
'banner_images' => [[
|
||||||
|
'image' => $original->key,
|
||||||
|
'crop_horizontal' => ['start_percentage' => 20, 'end_percentage' => 80],
|
||||||
|
'crop_vertical' => ['start_percentage' => 0, 'end_percentage' => 100],
|
||||||
|
]],
|
||||||
|
])->assertOk()
|
||||||
|
->assertJsonPath('data.banner_images.0.key', $original->key)
|
||||||
|
->assertJsonPath('data.banner_images.0.crop.crop_horizontal.start_percentage', 20);
|
||||||
|
$this->assertSame([$original->id], $tenant->websiteExtras()
|
||||||
|
->where('website_type_extra_id', $carousel->id)->firstOrFail()->config);
|
||||||
|
$this->assertNotSame($response->json('data.banner_images.0.url'),
|
||||||
|
$updated->json('data.banner_images.0.url'));
|
||||||
|
|
||||||
|
$this->assertSame('#abcdef', $tenant->refresh()->footer_bg_color);
|
||||||
|
$tenant->update(['primary_color' => '#123456']);
|
||||||
|
$this->assertSame('#abcdef', $tenant->refresh()->footer_bg_color);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_footer_color_is_mapped_to_a_supported_option(): void
|
||||||
|
{
|
||||||
|
$websiteType = $this->websiteType('shopit');
|
||||||
|
$cases = [
|
||||||
|
['primary', '#112233', '#112233'],
|
||||||
|
['white', '#112233', '#FFFFFF'],
|
||||||
|
['black', '#112233', '#000000'],
|
||||||
|
['primary', '#112233', '#445566'],
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($cases as $index => [$expected, $primaryColor, $footerColor]) {
|
||||||
|
$tenant = $this->tenant("tenant-{$index}", $websiteType, [
|
||||||
|
'primary_color' => $primaryColor,
|
||||||
|
'footer_bg_color' => $footerColor,
|
||||||
|
]);
|
||||||
|
Sanctum::actingAs($this->admin($tenant));
|
||||||
|
|
||||||
|
$this->getJson('/api/v1/adminapp/tenant/brand')
|
||||||
|
->assertOk()
|
||||||
|
->assertJsonPath('data.footer_color', $expected);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_missing_and_non_shopit_carousels_return_an_empty_list(): void
|
||||||
|
{
|
||||||
|
$shopit = $this->websiteType('shopit');
|
||||||
|
$tenant = $this->tenant('acme', $shopit);
|
||||||
|
Sanctum::actingAs($this->admin($tenant));
|
||||||
|
|
||||||
|
$this->getJson('/api/v1/adminapp/tenant/brand')
|
||||||
|
->assertOk()
|
||||||
|
->assertJsonPath('data.header_logo', null)
|
||||||
|
->assertJsonPath('data.footer_logo', null)
|
||||||
|
->assertJsonPath('data.banner_images', []);
|
||||||
|
|
||||||
|
$otherType = $this->websiteType('other');
|
||||||
|
$carousel = $otherType->extras()->create([
|
||||||
|
'codigo' => 'carousel',
|
||||||
|
'nombre' => 'Other carousel',
|
||||||
|
'is_required' => false,
|
||||||
|
'config_schema' => [
|
||||||
|
'request_rules' => ['$' => 'required|array'],
|
||||||
|
'transforms' => ['$.*' => ['handler' => 'attachment', 'attachment_type' => 'image']],
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
$other = $this->tenant('other', $otherType);
|
||||||
|
$other->websiteExtras()->create([
|
||||||
|
'website_type_extra_id' => $carousel->id,
|
||||||
|
'config' => [$this->image('other.png')->id],
|
||||||
|
]);
|
||||||
|
Sanctum::actingAs($this->admin($other));
|
||||||
|
|
||||||
|
$this->getJson('/api/v1/adminapp/tenant/brand')
|
||||||
|
->assertOk()
|
||||||
|
->assertJsonPath('data.banner_images', []);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_the_brand_endpoint_requires_an_adminapp_session(): void
|
||||||
|
{
|
||||||
|
$this->getJson('/api/v1/adminapp/tenant/brand')->assertUnauthorized();
|
||||||
|
|
||||||
|
Sanctum::actingAs(User::factory()->create([
|
||||||
|
'rol_codigo' => RoleCode::User->value,
|
||||||
|
'tenant_codigo' => null,
|
||||||
|
]));
|
||||||
|
|
||||||
|
$this->getJson('/api/v1/adminapp/tenant/brand')->assertForbidden();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function websiteType(string $code): StorefrontWebsiteType
|
||||||
|
{
|
||||||
|
return StorefrontWebsiteType::query()->create([
|
||||||
|
'codigo' => $code,
|
||||||
|
'nombre' => ucfirst($code),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function tenant(string $code, StorefrontWebsiteType $websiteType, array $attributes = []): Tenant
|
||||||
|
{
|
||||||
|
return Tenant::query()->create([
|
||||||
|
'codigo' => $code,
|
||||||
|
'nombre' => ucfirst($code),
|
||||||
|
'dominio' => "{$code}.test",
|
||||||
|
'storefront_website_type_code' => $websiteType->codigo,
|
||||||
|
...$attributes,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function admin(Tenant $tenant): User
|
||||||
|
{
|
||||||
|
return User::factory()->create([
|
||||||
|
'rol_codigo' => RoleCode::AdminApp->value,
|
||||||
|
'tenant_codigo' => $tenant->codigo,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function image(string $filename): Attachment
|
||||||
|
{
|
||||||
|
return Attachment::query()->create([
|
||||||
|
'path' => "tenants/test/{$filename}",
|
||||||
|
'filename' => $filename,
|
||||||
|
'type' => AttachmentType::Image,
|
||||||
|
'mime_type' => 'image/png',
|
||||||
|
'extension' => 'png',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
176
tests/Feature/Tenant/AdminAppHelpMenuControllerTest.php
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Feature\Tenant;
|
||||||
|
|
||||||
|
use App\Domains\Core\Auth\Models\User;
|
||||||
|
use App\Domains\Core\Authorization\Enums\RoleCode;
|
||||||
|
use App\Domains\Core\Menu\Models\Menu;
|
||||||
|
use App\Domains\Core\Tenant\Models\Tenant;
|
||||||
|
use Database\Seeders\AuthorizationSeeder;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
use Laravel\Sanctum\Sanctum;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
class AdminAppHelpMenuControllerTest extends TestCase
|
||||||
|
{
|
||||||
|
use RefreshDatabase;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
|
||||||
|
$this->seed(AuthorizationSeeder::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_authentication_is_required(): void
|
||||||
|
{
|
||||||
|
$this->getJson('/api/v1/adminapp/tenant/help-menus')
|
||||||
|
->assertUnauthorized();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_adminapp_user_can_read_help_menu_states(): void
|
||||||
|
{
|
||||||
|
[$tenant] = $this->tenantWithHelpMenus();
|
||||||
|
Sanctum::actingAs($this->createAdminAppUser($tenant));
|
||||||
|
|
||||||
|
$response = $this->getJson('/api/v1/adminapp/tenant/help-menus')
|
||||||
|
->assertOk();
|
||||||
|
|
||||||
|
$states = $response->json('data');
|
||||||
|
$this->assertFalse($states['help.faq']['is_enabled']);
|
||||||
|
$this->assertTrue($states['help.terms-and-conditions']['is_enabled']);
|
||||||
|
$this->assertSame('¿Cómo compro?', $states['help.faq']['static_content'][0]['pregunta']);
|
||||||
|
$this->assertSame(['html' => ''], $states['help.terms-and-conditions']['static_content']);
|
||||||
|
$this->assertArrayNotHasKey('help.contact', $states);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_toggling_a_help_menu_preserves_its_static_content(): void
|
||||||
|
{
|
||||||
|
[$tenant, $faq, $content] = $this->tenantWithHelpMenus();
|
||||||
|
Sanctum::actingAs($this->createAdminAppUser($tenant));
|
||||||
|
|
||||||
|
$this->patchJson('/api/v1/adminapp/tenant/help-menus/help.faq/toggle')
|
||||||
|
->assertOk()
|
||||||
|
->assertJsonPath('data.code', 'help.faq')
|
||||||
|
->assertJsonPath('data.is_enabled', true);
|
||||||
|
|
||||||
|
$pivot = $tenant->menues()->where('menues.code', $faq->code)->firstOrFail()->pivot;
|
||||||
|
|
||||||
|
$this->assertTrue($pivot->is_enabled);
|
||||||
|
$this->assertSame($content, $pivot->static_content);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_an_unrelated_menu_cannot_be_toggled_from_the_help_endpoint(): void
|
||||||
|
{
|
||||||
|
[$tenant] = $this->tenantWithHelpMenus();
|
||||||
|
Sanctum::actingAs($this->createAdminAppUser($tenant));
|
||||||
|
|
||||||
|
$this->patchJson('/api/v1/adminapp/tenant/help-menus/catalog/toggle')
|
||||||
|
->assertNotFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_contact_menu_cannot_be_toggled(): void
|
||||||
|
{
|
||||||
|
[$tenant] = $this->tenantWithHelpMenus();
|
||||||
|
Sanctum::actingAs($this->createAdminAppUser($tenant));
|
||||||
|
|
||||||
|
$this->patchJson('/api/v1/adminapp/tenant/help-menus/help.contact/toggle')
|
||||||
|
->assertNotFound();
|
||||||
|
|
||||||
|
$this->assertTrue(
|
||||||
|
$tenant->menues()->where('menues.code', 'help.contact')->firstOrFail()->pivot->is_enabled
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_adminapp_user_can_update_faq_content_without_changing_its_state(): void
|
||||||
|
{
|
||||||
|
[$tenant] = $this->tenantWithHelpMenus();
|
||||||
|
Sanctum::actingAs($this->createAdminAppUser($tenant));
|
||||||
|
$content = [[
|
||||||
|
'pregunta' => '¿Cuál es el horario?',
|
||||||
|
'respuesta' => 'De lunes a viernes.',
|
||||||
|
'is_active' => true,
|
||||||
|
]];
|
||||||
|
|
||||||
|
$this->putJson('/api/v1/adminapp/tenant/help-menus/help.faq/content', [
|
||||||
|
'static_content' => $content,
|
||||||
|
])
|
||||||
|
->assertOk()
|
||||||
|
->assertJsonPath('data.is_enabled', false)
|
||||||
|
->assertJsonPath('data.static_content.0.pregunta', '¿Cuál es el horario?');
|
||||||
|
|
||||||
|
$pivot = $tenant->menues()->where('menues.code', 'help.faq')->firstOrFail()->pivot;
|
||||||
|
$this->assertFalse($pivot->is_enabled);
|
||||||
|
$this->assertSame($content, $pivot->static_content);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_adminapp_user_can_update_terms_content(): void
|
||||||
|
{
|
||||||
|
[$tenant] = $this->tenantWithHelpMenus();
|
||||||
|
Sanctum::actingAs($this->createAdminAppUser($tenant));
|
||||||
|
|
||||||
|
$this->putJson('/api/v1/adminapp/tenant/help-menus/help.terms-and-conditions/content', [
|
||||||
|
'static_content' => ['html' => '<p>Nuevos términos</p>'],
|
||||||
|
])
|
||||||
|
->assertOk()
|
||||||
|
->assertJsonPath('data.static_content.html', '<p>Nuevos términos</p>');
|
||||||
|
|
||||||
|
$this->assertSame(
|
||||||
|
['html' => '<p>Nuevos términos</p>'],
|
||||||
|
$tenant->menues()
|
||||||
|
->where('menues.code', 'help.terms-and-conditions')
|
||||||
|
->firstOrFail()
|
||||||
|
->pivot
|
||||||
|
->static_content
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return array{Tenant, Menu, array<int, array<string, mixed>>} */
|
||||||
|
private function tenantWithHelpMenus(): array
|
||||||
|
{
|
||||||
|
$tenant = Tenant::query()->create([
|
||||||
|
'codigo' => 'acme',
|
||||||
|
'nombre' => 'Acme',
|
||||||
|
'dominio' => 'acme.test',
|
||||||
|
]);
|
||||||
|
$content = [[
|
||||||
|
'pregunta' => '¿Cómo compro?',
|
||||||
|
'respuesta' => 'Desde el catálogo.',
|
||||||
|
'is_active' => true,
|
||||||
|
]];
|
||||||
|
$faq = Menu::query()->create([
|
||||||
|
'code' => 'help.faq',
|
||||||
|
'label' => 'Preguntas frecuentes',
|
||||||
|
'content_type' => Menu::CONTENT_TYPE_STATIC,
|
||||||
|
'static_content_schema' => ['*.pregunta' => 'required|string'],
|
||||||
|
'route' => '/ayuda/preguntas-frecuentes',
|
||||||
|
]);
|
||||||
|
$contact = Menu::query()->create([
|
||||||
|
'code' => 'help.contact',
|
||||||
|
'label' => 'Contacto',
|
||||||
|
'route' => '/ayuda/contacto',
|
||||||
|
]);
|
||||||
|
$terms = Menu::query()->create([
|
||||||
|
'code' => 'help.terms-and-conditions',
|
||||||
|
'label' => 'Términos y condiciones',
|
||||||
|
'route' => '/ayuda/terminos-y-condiciones',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$tenant->menues()->attach($faq->code, [
|
||||||
|
'static_content' => $content,
|
||||||
|
'is_enabled' => false,
|
||||||
|
]);
|
||||||
|
$tenant->menues()->attach($contact->code, ['is_enabled' => true]);
|
||||||
|
$tenant->menues()->attach($terms->code, ['is_enabled' => true]);
|
||||||
|
|
||||||
|
return [$tenant, $faq, $content];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function createAdminAppUser(Tenant $tenant): User
|
||||||
|
{
|
||||||
|
return User::factory()->create([
|
||||||
|
'rol_codigo' => RoleCode::AdminApp->value,
|
||||||
|
'tenant_codigo' => $tenant->codigo,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -510,7 +510,7 @@ class BootstrapTenantControllerTest extends TestCase
|
|||||||
$tenant->menues()->sync([
|
$tenant->menues()->sync([
|
||||||
$parent->code,
|
$parent->code,
|
||||||
$staticChild->code => ['static_content' => $staticContent],
|
$staticChild->code => ['static_content' => $staticContent],
|
||||||
$dynamicChild->code,
|
$dynamicChild->code => ['static_content' => ['legacy' => 'must not be returned']],
|
||||||
]);
|
]);
|
||||||
$userRole->menus()->sync([
|
$userRole->menus()->sync([
|
||||||
$parent->code,
|
$parent->code,
|
||||||
|
|||||||