Compare commits
10 Commits
b94efea4e0
...
0cb1c37566
| Author | SHA1 | Date | |
|---|---|---|---|
| 0cb1c37566 | |||
| 057e6757dc | |||
| 6d066beea3 | |||
| 1d94ff8885 | |||
| ee3be8d550 | |||
| 21b8fa5f35 | |||
| 3887a4fcba | |||
| 0d4c6a4492 | |||
| c1b7b12fe1 | |||
| b1e40b3430 |
@@ -4,6 +4,7 @@ APP_KEY=
|
||||
APP_DEBUG=true
|
||||
APP_URL=http://localhost
|
||||
FRONTEND_URL=http://localhost:4200
|
||||
INTEGRATION_SECRET=
|
||||
|
||||
APP_LOCALE=en
|
||||
APP_FALLBACK_LOCALE=en
|
||||
|
||||
@@ -898,7 +898,7 @@
|
||||
}
|
||||
],
|
||||
"url": {
|
||||
"raw": "{{base_url}}/api/tenants/{{tenant_codigo}}/product-variants",
|
||||
"raw": "{{base_url}}/api/tenants/{{tenant_codigo}}/productos/{{producto_id}}/variants",
|
||||
"host": [
|
||||
"{{base_url}}"
|
||||
],
|
||||
@@ -906,7 +906,9 @@
|
||||
"api",
|
||||
"tenants",
|
||||
"{{tenant_codigo}}",
|
||||
"product-variants"
|
||||
"productos",
|
||||
"{{producto_id}}",
|
||||
"variants"
|
||||
]
|
||||
}
|
||||
},
|
||||
@@ -930,10 +932,10 @@
|
||||
],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"producto_id\": {{producto_id}},\n \"slug\": \"remera-basica-blanca-m\",\n \"nombre\": \"Remera Basica Blanca - Talle M\",\n \"stock\": 8,\n \"descripcion\": \"Variante de prueba\",\n \"precio\": 26999,\n \"definitions\": [\n {\n \"products_attribute_id\": {{color_product_attribute_id}},\n \"value\": \"Blanco\"\n },\n {\n \"products_attribute_id\": {{talle_product_attribute_id}},\n \"value\": \"M\"\n }\n ],\n \"images\": []\n}"
|
||||
"raw": "{\n \"stock\": 8,\n \"definitions\": [\n {\n \"products_attribute_id\": {{color_product_attribute_id}},\n \"value\": \"Blanco\"\n },\n {\n \"products_attribute_id\": {{talle_product_attribute_id}},\n \"value\": \"M\"\n }\n ],\n \"images\": []\n}"
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{base_url}}/api/tenants/{{tenant_codigo}}/product-variants",
|
||||
"raw": "{{base_url}}/api/tenants/{{tenant_codigo}}/productos/{{producto_id}}/variants",
|
||||
"host": [
|
||||
"{{base_url}}"
|
||||
],
|
||||
@@ -941,7 +943,9 @@
|
||||
"api",
|
||||
"tenants",
|
||||
"{{tenant_codigo}}",
|
||||
"product-variants"
|
||||
"productos",
|
||||
"{{producto_id}}",
|
||||
"variants"
|
||||
]
|
||||
}
|
||||
},
|
||||
@@ -959,7 +963,7 @@
|
||||
}
|
||||
],
|
||||
"url": {
|
||||
"raw": "{{base_url}}/api/tenants/{{tenant_codigo}}/product-variants/{{product_variant_id}}",
|
||||
"raw": "{{base_url}}/api/tenants/{{tenant_codigo}}/productos/{{producto_id}}/variants/{{product_variant_id}}",
|
||||
"host": [
|
||||
"{{base_url}}"
|
||||
],
|
||||
@@ -967,7 +971,9 @@
|
||||
"api",
|
||||
"tenants",
|
||||
"{{tenant_codigo}}",
|
||||
"product-variants",
|
||||
"productos",
|
||||
"{{producto_id}}",
|
||||
"variants",
|
||||
"{{product_variant_id}}"
|
||||
]
|
||||
}
|
||||
@@ -992,10 +998,10 @@
|
||||
],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"slug\": \"remera-basica-blanca-m\",\n \"nombre\": \"Remera Basica Blanca - Talle M Updated\",\n \"stock\": 12,\n \"descripcion\": \"Variante actualizada\",\n \"precio\": 27999,\n \"definitions\": [\n {\n \"products_attribute_id\": {{color_product_attribute_id}},\n \"value\": \"Blanco\"\n },\n {\n \"products_attribute_id\": {{talle_product_attribute_id}},\n \"value\": \"M\"\n }\n ],\n \"images\": []\n}"
|
||||
"raw": "{\n \"stock\": 12,\n \"definitions\": [\n {\n \"products_attribute_id\": {{color_product_attribute_id}},\n \"value\": \"Blanco\"\n },\n {\n \"products_attribute_id\": {{talle_product_attribute_id}},\n \"value\": \"M\"\n }\n ],\n \"images\": []\n}"
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{base_url}}/api/tenants/{{tenant_codigo}}/product-variants/{{product_variant_id}}",
|
||||
"raw": "{{base_url}}/api/tenants/{{tenant_codigo}}/productos/{{producto_id}}/variants/{{product_variant_id}}",
|
||||
"host": [
|
||||
"{{base_url}}"
|
||||
],
|
||||
@@ -1003,7 +1009,9 @@
|
||||
"api",
|
||||
"tenants",
|
||||
"{{tenant_codigo}}",
|
||||
"product-variants",
|
||||
"productos",
|
||||
"{{producto_id}}",
|
||||
"variants",
|
||||
"{{product_variant_id}}"
|
||||
]
|
||||
}
|
||||
@@ -1022,7 +1030,7 @@
|
||||
}
|
||||
],
|
||||
"url": {
|
||||
"raw": "{{base_url}}/api/tenants/{{tenant_codigo}}/product-variants/{{product_variant_id}}",
|
||||
"raw": "{{base_url}}/api/tenants/{{tenant_codigo}}/productos/{{producto_id}}/variants/{{product_variant_id}}",
|
||||
"host": [
|
||||
"{{base_url}}"
|
||||
],
|
||||
@@ -1030,7 +1038,9 @@
|
||||
"api",
|
||||
"tenants",
|
||||
"{{tenant_codigo}}",
|
||||
"product-variants",
|
||||
"productos",
|
||||
"{{producto_id}}",
|
||||
"variants",
|
||||
"{{product_variant_id}}"
|
||||
]
|
||||
}
|
||||
@@ -1403,6 +1413,454 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Bank Accounts",
|
||||
"item": [
|
||||
{
|
||||
"name": "List Bank Accounts",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{
|
||||
"key": "Accept",
|
||||
"value": "application/json",
|
||||
"type": "text"
|
||||
}
|
||||
],
|
||||
"url": {
|
||||
"raw": "{{base_url}}/api/tenants/{{tenant_codigo}}/bank-accounts",
|
||||
"host": [
|
||||
"{{base_url}}"
|
||||
],
|
||||
"path": [
|
||||
"api",
|
||||
"tenants",
|
||||
"{{tenant_codigo}}",
|
||||
"bank-accounts"
|
||||
]
|
||||
}
|
||||
},
|
||||
"response": []
|
||||
},
|
||||
{
|
||||
"name": "Create Bank Account",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{
|
||||
"key": "Accept",
|
||||
"value": "application/json",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"value": "application/json",
|
||||
"type": "text"
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"titular\": \"Ada Lovelace\",\n \"entidad\": \"Banco Demo\",\n \"alias\": \"ada.demo.alias\",\n \"cvu\": \"0000003100000000000001\"\n}"
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{base_url}}/api/tenants/{{tenant_codigo}}/bank-accounts",
|
||||
"host": [
|
||||
"{{base_url}}"
|
||||
],
|
||||
"path": [
|
||||
"api",
|
||||
"tenants",
|
||||
"{{tenant_codigo}}",
|
||||
"bank-accounts"
|
||||
]
|
||||
}
|
||||
},
|
||||
"response": []
|
||||
},
|
||||
{
|
||||
"name": "Show Selected Bank Account",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{
|
||||
"key": "Accept",
|
||||
"value": "application/json",
|
||||
"type": "text"
|
||||
}
|
||||
],
|
||||
"url": {
|
||||
"raw": "{{base_url}}/api/tenants/{{tenant_codigo}}/bank-accounts/selected",
|
||||
"host": [
|
||||
"{{base_url}}"
|
||||
],
|
||||
"path": [
|
||||
"api",
|
||||
"tenants",
|
||||
"{{tenant_codigo}}",
|
||||
"bank-accounts",
|
||||
"selected"
|
||||
]
|
||||
}
|
||||
},
|
||||
"response": []
|
||||
},
|
||||
{
|
||||
"name": "Show Bank Account",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{
|
||||
"key": "Accept",
|
||||
"value": "application/json",
|
||||
"type": "text"
|
||||
}
|
||||
],
|
||||
"url": {
|
||||
"raw": "{{base_url}}/api/tenants/{{tenant_codigo}}/bank-accounts/{{bank_account_id}}",
|
||||
"host": [
|
||||
"{{base_url}}"
|
||||
],
|
||||
"path": [
|
||||
"api",
|
||||
"tenants",
|
||||
"{{tenant_codigo}}",
|
||||
"bank-accounts",
|
||||
"{{bank_account_id}}"
|
||||
]
|
||||
}
|
||||
},
|
||||
"response": []
|
||||
},
|
||||
{
|
||||
"name": "Update Bank Account",
|
||||
"request": {
|
||||
"method": "PATCH",
|
||||
"header": [
|
||||
{
|
||||
"key": "Accept",
|
||||
"value": "application/json",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"value": "application/json",
|
||||
"type": "text"
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"titular\": \"Ada Lovelace Updated\",\n \"entidad\": \"Banco Demo Updated\",\n \"alias\": \"ada.demo.updated\",\n \"cvu\": \"0000003100000000000002\"\n}"
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{base_url}}/api/tenants/{{tenant_codigo}}/bank-accounts/{{bank_account_id}}",
|
||||
"host": [
|
||||
"{{base_url}}"
|
||||
],
|
||||
"path": [
|
||||
"api",
|
||||
"tenants",
|
||||
"{{tenant_codigo}}",
|
||||
"bank-accounts",
|
||||
"{{bank_account_id}}"
|
||||
]
|
||||
}
|
||||
},
|
||||
"response": []
|
||||
},
|
||||
{
|
||||
"name": "Select Bank Account",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{
|
||||
"key": "Accept",
|
||||
"value": "application/json",
|
||||
"type": "text"
|
||||
}
|
||||
],
|
||||
"url": {
|
||||
"raw": "{{base_url}}/api/tenants/{{tenant_codigo}}/bank-accounts/{{bank_account_id}}/select",
|
||||
"host": [
|
||||
"{{base_url}}"
|
||||
],
|
||||
"path": [
|
||||
"api",
|
||||
"tenants",
|
||||
"{{tenant_codigo}}",
|
||||
"bank-accounts",
|
||||
"{{bank_account_id}}",
|
||||
"select"
|
||||
]
|
||||
}
|
||||
},
|
||||
"response": []
|
||||
},
|
||||
{
|
||||
"name": "Delete Bank Account",
|
||||
"request": {
|
||||
"method": "DELETE",
|
||||
"header": [
|
||||
{
|
||||
"key": "Accept",
|
||||
"value": "application/json",
|
||||
"type": "text"
|
||||
}
|
||||
],
|
||||
"url": {
|
||||
"raw": "{{base_url}}/api/tenants/{{tenant_codigo}}/bank-accounts/{{bank_account_id}}",
|
||||
"host": [
|
||||
"{{base_url}}"
|
||||
],
|
||||
"path": [
|
||||
"api",
|
||||
"tenants",
|
||||
"{{tenant_codigo}}",
|
||||
"bank-accounts",
|
||||
"{{bank_account_id}}"
|
||||
]
|
||||
}
|
||||
},
|
||||
"response": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Integrations",
|
||||
"item": [
|
||||
{
|
||||
"name": "List Integrations",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{
|
||||
"key": "Accept",
|
||||
"value": "application/json",
|
||||
"type": "text"
|
||||
}
|
||||
],
|
||||
"url": {
|
||||
"raw": "{{base_url}}/api/integrations",
|
||||
"host": [
|
||||
"{{base_url}}"
|
||||
],
|
||||
"path": [
|
||||
"api",
|
||||
"integrations"
|
||||
]
|
||||
}
|
||||
},
|
||||
"response": []
|
||||
},
|
||||
{
|
||||
"name": "Create Integration",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{
|
||||
"key": "Accept",
|
||||
"value": "application/json",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"value": "application/json",
|
||||
"type": "text"
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"integration_code\": \"mercadopago\",\n \"name\": \"Mercado Pago\",\n \"url\": \"https://api.mercadopago.com\",\n \"integration_data_schema\": {\n \"access_token\": \"required|string\",\n \"public_key\": \"nullable|string\"\n }\n}"
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{base_url}}/api/integrations",
|
||||
"host": [
|
||||
"{{base_url}}"
|
||||
],
|
||||
"path": [
|
||||
"api",
|
||||
"integrations"
|
||||
]
|
||||
}
|
||||
},
|
||||
"response": []
|
||||
},
|
||||
{
|
||||
"name": "Show Integration",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{
|
||||
"key": "Accept",
|
||||
"value": "application/json",
|
||||
"type": "text"
|
||||
}
|
||||
],
|
||||
"url": {
|
||||
"raw": "{{base_url}}/api/integrations/{{integration_id}}",
|
||||
"host": [
|
||||
"{{base_url}}"
|
||||
],
|
||||
"path": [
|
||||
"api",
|
||||
"integrations",
|
||||
"{{integration_id}}"
|
||||
]
|
||||
}
|
||||
},
|
||||
"response": []
|
||||
},
|
||||
{
|
||||
"name": "Update Integration",
|
||||
"request": {
|
||||
"method": "PUT",
|
||||
"header": [
|
||||
{
|
||||
"key": "Accept",
|
||||
"value": "application/json",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"value": "application/json",
|
||||
"type": "text"
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"integration_code\": \"mercadopago\",\n \"name\": \"Mercado Pago Updated\",\n \"url\": \"https://api.mercadopago.com\",\n \"integration_data_schema\": {\n \"access_token\": \"required|string\",\n \"public_key\": \"nullable|string\"\n }\n}"
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{base_url}}/api/integrations/{{integration_id}}",
|
||||
"host": [
|
||||
"{{base_url}}"
|
||||
],
|
||||
"path": [
|
||||
"api",
|
||||
"integrations",
|
||||
"{{integration_id}}"
|
||||
]
|
||||
}
|
||||
},
|
||||
"response": []
|
||||
},
|
||||
{
|
||||
"name": "Delete Integration",
|
||||
"request": {
|
||||
"method": "DELETE",
|
||||
"header": [
|
||||
{
|
||||
"key": "Accept",
|
||||
"value": "application/json",
|
||||
"type": "text"
|
||||
}
|
||||
],
|
||||
"url": {
|
||||
"raw": "{{base_url}}/api/integrations/{{integration_id}}",
|
||||
"host": [
|
||||
"{{base_url}}"
|
||||
],
|
||||
"path": [
|
||||
"api",
|
||||
"integrations",
|
||||
"{{integration_id}}"
|
||||
]
|
||||
}
|
||||
},
|
||||
"response": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Tenant Integrations",
|
||||
"item": [
|
||||
{
|
||||
"name": "List Tenant Integrations",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{
|
||||
"key": "Accept",
|
||||
"value": "application/json",
|
||||
"type": "text"
|
||||
}
|
||||
],
|
||||
"url": {
|
||||
"raw": "{{base_url}}/api/{{tenant_codigo}}/integrations",
|
||||
"host": [
|
||||
"{{base_url}}"
|
||||
],
|
||||
"path": [
|
||||
"api",
|
||||
"{{tenant_codigo}}",
|
||||
"integrations"
|
||||
]
|
||||
}
|
||||
},
|
||||
"response": []
|
||||
},
|
||||
{
|
||||
"name": "Show Tenant Integration",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{
|
||||
"key": "Accept",
|
||||
"value": "application/json",
|
||||
"type": "text"
|
||||
}
|
||||
],
|
||||
"url": {
|
||||
"raw": "{{base_url}}/api/{{tenant_codigo}}/integrations/{{integration_code}}",
|
||||
"host": [
|
||||
"{{base_url}}"
|
||||
],
|
||||
"path": [
|
||||
"api",
|
||||
"{{tenant_codigo}}",
|
||||
"integrations",
|
||||
"{{integration_code}}"
|
||||
]
|
||||
}
|
||||
},
|
||||
"response": []
|
||||
},
|
||||
{
|
||||
"name": "Configure Tenant Integration",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{
|
||||
"key": "Accept",
|
||||
"value": "application/json",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"value": "application/json",
|
||||
"type": "text"
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"integration_data\": {\n \"access_token\": \"APP_USR-demo-token\",\n \"public_key\": \"APP_PUBLIC-demo-key\"\n }\n}"
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{base_url}}/api/{{tenant_codigo}}/integrations/{{integration_code}}",
|
||||
"host": [
|
||||
"{{base_url}}"
|
||||
],
|
||||
"path": [
|
||||
"api",
|
||||
"{{tenant_codigo}}",
|
||||
"integrations",
|
||||
"{{integration_code}}"
|
||||
]
|
||||
}
|
||||
},
|
||||
"response": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Storage Test",
|
||||
"item": [
|
||||
@@ -1628,6 +2086,21 @@
|
||||
"key": "compra_id",
|
||||
"value": "1",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"key": "integration_id",
|
||||
"value": "1",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"key": "integration_code",
|
||||
"value": "mercadopago",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"key": "bank_account_id",
|
||||
"value": "1",
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
use Laravel\Sanctum\HasApiTokens;
|
||||
|
||||
#[Fillable(['nombre_apellido', 'email', 'password'])]
|
||||
#[Fillable(['nombre_apellido', 'email', 'password', 'dni', 'telefono'])]
|
||||
#[Hidden(['password', 'remember_token'])]
|
||||
class User extends Authenticatable
|
||||
{
|
||||
|
||||
@@ -21,6 +21,8 @@ class RegisterUserRequest extends FormRequest
|
||||
'nombre_apellido' => ['required', 'string', 'max:255'],
|
||||
'email' => ['required', 'string', 'email', 'max:255', Rule::unique('users', 'email')],
|
||||
'password' => ['required', 'string', 'confirmed'],
|
||||
'dni' => ['nullable', 'string', 'max:255'],
|
||||
'telefono' => ['nullable', 'string', 'max:255'],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,8 @@ class UserResource extends JsonResource
|
||||
'id' => $this->id,
|
||||
'nombre_apellido' => $this->nombre_apellido,
|
||||
'email' => $this->email,
|
||||
'dni' => $this->dni,
|
||||
'telefono' => $this->telefono,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ use App\Domains\Auth\Models\User;
|
||||
class RegisterUserService
|
||||
{
|
||||
/**
|
||||
* @param array{nombre_apellido: string, email: string, password: string} $data
|
||||
* @param array{nombre_apellido: string, email: string, password: string, dni?: string|null, telefono?: string|null} $data
|
||||
*/
|
||||
public function register(array $data): User
|
||||
{
|
||||
@@ -15,6 +15,8 @@ class RegisterUserService
|
||||
'nombre_apellido' => $data['nombre_apellido'],
|
||||
'email' => $data['email'],
|
||||
'password' => $data['password'],
|
||||
'dni' => $data['dni'] ?? null,
|
||||
'telefono' => $data['telefono'] ?? null,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\BankAccount\Controllers;
|
||||
|
||||
use App\Domains\BankAccount\Models\BankAccount;
|
||||
use App\Domains\Tenant\Models\Tenant;
|
||||
use App\Domains\BankAccount\Requests\StoreBankAccountRequest;
|
||||
use App\Domains\BankAccount\Requests\UpdateBankAccountRequest;
|
||||
use App\Domains\BankAccount\Resources\BankAccountResource;
|
||||
use App\Domains\Tenant\Resources\TenantResource;
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Response;
|
||||
|
||||
class BankAccountController extends Controller
|
||||
{
|
||||
public function index(Tenant $tenant): JsonResponse
|
||||
{
|
||||
$accounts = $tenant->bankAccounts()->latest()->get();
|
||||
return BankAccountResource::collection($accounts)->response();
|
||||
}
|
||||
|
||||
public function store(StoreBankAccountRequest $request, Tenant $tenant): JsonResponse
|
||||
{
|
||||
$data = $request->validated();
|
||||
$data['tenant_code'] = $tenant->codigo;
|
||||
|
||||
$account = BankAccount::query()->create($data);
|
||||
|
||||
return BankAccountResource::make($account)->response()->setStatusCode(201);
|
||||
}
|
||||
|
||||
public function show(Tenant $tenant, BankAccount $bankAccount): BankAccountResource
|
||||
{
|
||||
abort_if($bankAccount->tenant_code !== $tenant->codigo, 404);
|
||||
|
||||
return BankAccountResource::make($bankAccount);
|
||||
}
|
||||
|
||||
public function update(UpdateBankAccountRequest $request, Tenant $tenant, BankAccount $bankAccount): BankAccountResource
|
||||
{
|
||||
abort_if($bankAccount->tenant_code !== $tenant->codigo, 404);
|
||||
|
||||
$bankAccount->update($request->validated());
|
||||
|
||||
return BankAccountResource::make($bankAccount);
|
||||
}
|
||||
|
||||
public function destroy(Tenant $tenant, BankAccount $bankAccount): Response
|
||||
{
|
||||
abort_if($bankAccount->tenant_code !== $tenant->codigo, 404);
|
||||
|
||||
$bankAccount->delete();
|
||||
|
||||
return response()->noContent();
|
||||
}
|
||||
|
||||
public function select(Tenant $tenant, BankAccount $bankAccount): TenantResource
|
||||
{
|
||||
abort_if($bankAccount->tenant_code !== $tenant->codigo, 400, 'La cuenta bancaria no pertenece a este tenant.');
|
||||
|
||||
$tenant->selected_bank_account_id = $bankAccount->id;
|
||||
$tenant->save();
|
||||
|
||||
return TenantResource::make($tenant->loadMissing(['headerLogo', 'footerLogo']));
|
||||
}
|
||||
|
||||
public function selected(Tenant $tenant): JsonResponse
|
||||
{
|
||||
$account = $tenant->selectedBankAccount;
|
||||
|
||||
abort_if(!$account, 404, 'No hay ninguna cuenta bancaria seleccionada para este tenant.');
|
||||
|
||||
return BankAccountResource::make($account)->response();
|
||||
}
|
||||
}
|
||||
29
app/Domains/BankAccount/Models/BankAccount.php
Normal file
29
app/Domains/BankAccount/Models/BankAccount.php
Normal file
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\BankAccount\Models;
|
||||
|
||||
use App\Domains\Tenant\Models\Tenant;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class BankAccount extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'tenant_code',
|
||||
'titular',
|
||||
'entidad',
|
||||
'alias',
|
||||
'cvu',
|
||||
];
|
||||
|
||||
/**
|
||||
* @return BelongsTo<Tenant, $this>
|
||||
*/
|
||||
public function tenant(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Tenant::class, 'tenant_code', 'codigo');
|
||||
}
|
||||
}
|
||||
47
app/Domains/BankAccount/Requests/StoreBankAccountRequest.php
Normal file
47
app/Domains/BankAccount/Requests/StoreBankAccountRequest.php
Normal file
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\BankAccount\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class StoreBankAccountRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'titular' => ['required', 'string', 'max:255'],
|
||||
'entidad' => ['required', 'string', 'max:255'],
|
||||
'alias' => ['required', 'string', 'max:255'],
|
||||
'cvu' => ['required', 'string', 'max:255'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'titular.required' => 'El campo titular es obligatorio.',
|
||||
'titular.string' => 'El campo titular debe ser una cadena de texto.',
|
||||
'titular.max' => 'El campo titular no debe superar los 255 caracteres.',
|
||||
'entidad.required' => 'El campo entidad es obligatorio.',
|
||||
'entidad.string' => 'El campo entidad debe ser una cadena de texto.',
|
||||
'entidad.max' => 'El campo entidad no debe superar los 255 caracteres.',
|
||||
'alias.required' => 'El campo alias es obligatorio.',
|
||||
'alias.string' => 'El campo alias debe ser una cadena de texto.',
|
||||
'alias.max' => 'El campo alias no debe superar los 255 caracteres.',
|
||||
'cvu.required' => 'El campo cvu es obligatorio.',
|
||||
'cvu.string' => 'El campo cvu debe ser una cadena de texto.',
|
||||
'cvu.max' => 'El campo cvu no debe superar los 255 caracteres.',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\BankAccount\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class UpdateBankAccountRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'titular' => ['sometimes', 'string', 'max:255'],
|
||||
'entidad' => ['sometimes', 'string', 'max:255'],
|
||||
'alias' => ['sometimes', 'string', 'max:255'],
|
||||
'cvu' => ['sometimes', 'string', 'max:255'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'titular.string' => 'El campo titular debe ser una cadena de texto.',
|
||||
'titular.max' => 'El campo titular no debe superar los 255 caracteres.',
|
||||
'entidad.string' => 'El campo entidad debe ser una cadena de texto.',
|
||||
'entidad.max' => 'El campo entidad no debe superar los 255 caracteres.',
|
||||
'alias.string' => 'El campo alias debe ser una cadena de texto.',
|
||||
'alias.max' => 'El campo alias no debe superar los 255 caracteres.',
|
||||
'cvu.string' => 'El campo cvu debe ser una cadena de texto.',
|
||||
'cvu.max' => 'El campo cvu no debe superar los 255 caracteres.',
|
||||
];
|
||||
}
|
||||
}
|
||||
27
app/Domains/BankAccount/Resources/BankAccountResource.php
Normal file
27
app/Domains/BankAccount/Resources/BankAccountResource.php
Normal file
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\BankAccount\Resources;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
/**
|
||||
* @mixin \App\Domains\BankAccount\Models\BankAccount
|
||||
*/
|
||||
class BankAccountResource extends JsonResource
|
||||
{
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'tenant_code' => $this->tenant_code,
|
||||
'titular' => $this->titular,
|
||||
'entidad' => $this->entidad,
|
||||
'alias' => $this->alias,
|
||||
'cvu' => $this->cvu,
|
||||
];
|
||||
}
|
||||
}
|
||||
13
app/Domains/BankAccount/routes/api.php
Normal file
13
app/Domains/BankAccount/routes/api.php
Normal file
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
use App\Domains\BankAccount\Controllers\BankAccountController;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
Route::prefix('tenants/{tenant:codigo}')->group(function (): void {
|
||||
Route::middleware('auth:sanctum')->group(function (): void {
|
||||
Route::get('bank-accounts/selected', [BankAccountController::class, 'selected']);
|
||||
Route::post('bank-accounts/{bank_account}/select', [BankAccountController::class, 'select']);
|
||||
Route::apiResource('bank-accounts', BankAccountController::class)
|
||||
->parameters(['bank-accounts' => 'bank_account']);
|
||||
});
|
||||
});
|
||||
66
app/Domains/Integration/Casts/EncryptedIntegrationData.php
Normal file
66
app/Domains/Integration/Casts/EncryptedIntegrationData.php
Normal file
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Integration\Casts;
|
||||
|
||||
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Encryption\Encrypter;
|
||||
use Exception;
|
||||
|
||||
class EncryptedIntegrationData implements CastsAttributes
|
||||
{
|
||||
protected function getEncrypter(): Encrypter
|
||||
{
|
||||
$secret = config('services.integrations.secret');
|
||||
|
||||
if (empty($secret)) {
|
||||
throw new Exception('The integrations secret is not configured.');
|
||||
}
|
||||
|
||||
// Laravel encrypter requires a key of exact length. Typically 32 bytes for AES-256-CBC.
|
||||
// If the secret is base64 encoded like the APP_KEY:
|
||||
if (str_starts_with($secret, 'base64:')) {
|
||||
$key = base64_decode(substr($secret, 7));
|
||||
} else {
|
||||
// Otherwise, we hash it to ensure 32 bytes for AES-256-CBC.
|
||||
$key = hash('sha256', $secret, true);
|
||||
}
|
||||
|
||||
return new Encrypter($key, config('app.cipher', 'AES-256-CBC'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Cast the given value.
|
||||
*
|
||||
* @param array<string, mixed> $attributes
|
||||
*/
|
||||
public function get(Model $model, string $key, mixed $value, array $attributes): mixed
|
||||
{
|
||||
if ($value === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
$decrypted = $this->getEncrypter()->decryptString($value);
|
||||
return json_decode($decrypted, true);
|
||||
} catch (Exception $e) {
|
||||
// Return null or throw depending on how strict we want to be.
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare the given value for storage.
|
||||
*
|
||||
* @param array<string, mixed> $attributes
|
||||
*/
|
||||
public function set(Model $model, string $key, mixed $value, array $attributes): mixed
|
||||
{
|
||||
if ($value === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$json = json_encode($value);
|
||||
return $this->getEncrypter()->encryptString($json);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Integration\Controllers;
|
||||
|
||||
use App\Domains\Integration\Models\Integration;
|
||||
use App\Domains\Integration\Requests\StoreIntegrationRequest;
|
||||
use App\Domains\Integration\Requests\UpdateIntegrationRequest;
|
||||
use Illuminate\Routing\Controller;
|
||||
|
||||
class IntegrationController extends Controller
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
return response()->json(Integration::all());
|
||||
}
|
||||
|
||||
public function store(StoreIntegrationRequest $request)
|
||||
{
|
||||
$integration = Integration::create($request->validated());
|
||||
|
||||
return response()->json($integration, 201);
|
||||
}
|
||||
|
||||
public function show(Integration $integration)
|
||||
{
|
||||
return response()->json($integration);
|
||||
}
|
||||
|
||||
public function update(UpdateIntegrationRequest $request, Integration $integration)
|
||||
{
|
||||
$integration->update($request->validated());
|
||||
|
||||
return response()->json($integration->fresh());
|
||||
}
|
||||
|
||||
public function destroy(Integration $integration)
|
||||
{
|
||||
$integration->delete();
|
||||
|
||||
return response()->noContent();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Integration\Controllers;
|
||||
|
||||
use App\Domains\Integration\Models\Integration;
|
||||
use App\Domains\Integration\Requests\StoreTenantIntegrationRequest;
|
||||
use App\Domains\Integration\Services\TenantIntegrationService;
|
||||
use Illuminate\Routing\Controller;
|
||||
|
||||
class TenantIntegrationController extends Controller
|
||||
{
|
||||
protected TenantIntegrationService $tenantIntegrationService;
|
||||
|
||||
public function __construct(TenantIntegrationService $tenantIntegrationService)
|
||||
{
|
||||
$this->tenantIntegrationService = $tenantIntegrationService;
|
||||
}
|
||||
|
||||
public function index(string $tenantCode)
|
||||
{
|
||||
return response()->json($this->tenantIntegrationService->getAllForTenant($tenantCode));
|
||||
}
|
||||
|
||||
public function show(string $tenantCode, string $integrationCode)
|
||||
{
|
||||
$integration = $this->tenantIntegrationService->getTenantIntegration($tenantCode, $integrationCode);
|
||||
|
||||
if (!$integration) {
|
||||
return response()->json(['message' => 'Integration not configured for this tenant'], 404);
|
||||
}
|
||||
|
||||
return response()->json($integration);
|
||||
}
|
||||
|
||||
public function store(StoreTenantIntegrationRequest $request, string $tenantCode, string $integrationCode)
|
||||
{
|
||||
$integration = Integration::where('integration_code', $integrationCode)->firstOrFail();
|
||||
|
||||
$tenantIntegration = $this->tenantIntegrationService->updateOrCreateIntegration(
|
||||
$tenantCode,
|
||||
$integration,
|
||||
$request->input('integration_data', [])
|
||||
);
|
||||
|
||||
return response()->json($tenantIntegration);
|
||||
}
|
||||
}
|
||||
26
app/Domains/Integration/Models/Integration.php
Normal file
26
app/Domains/Integration/Models/Integration.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Integration\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Integration extends Model
|
||||
{
|
||||
protected $table = 'integrations';
|
||||
|
||||
protected $fillable = [
|
||||
'integration_code',
|
||||
'name',
|
||||
'url',
|
||||
'integration_data_schema',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'integration_data_schema' => 'array',
|
||||
];
|
||||
|
||||
public function tenantIntegrations()
|
||||
{
|
||||
return $this->hasMany(TenantIntegration::class, 'integration_code', 'integration_code');
|
||||
}
|
||||
}
|
||||
26
app/Domains/Integration/Models/TenantIntegration.php
Normal file
26
app/Domains/Integration/Models/TenantIntegration.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Integration\Models;
|
||||
|
||||
use App\Domains\Integration\Casts\EncryptedIntegrationData;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class TenantIntegration extends Model
|
||||
{
|
||||
protected $table = 'tenant_integration';
|
||||
|
||||
protected $fillable = [
|
||||
'integration_code',
|
||||
'tenant_code',
|
||||
'integration_data',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'integration_data' => EncryptedIntegrationData::class,
|
||||
];
|
||||
|
||||
public function integration()
|
||||
{
|
||||
return $this->belongsTo(Integration::class, 'integration_code', 'integration_code');
|
||||
}
|
||||
}
|
||||
23
app/Domains/Integration/Requests/StoreIntegrationRequest.php
Normal file
23
app/Domains/Integration/Requests/StoreIntegrationRequest.php
Normal file
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Integration\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class StoreIntegrationRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'integration_code' => ['required', 'string', 'unique:integrations,integration_code'],
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'url' => ['nullable', 'url', 'max:255'],
|
||||
'integration_data_schema' => ['nullable', 'array'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Integration\Requests;
|
||||
|
||||
use App\Domains\Integration\Models\Integration;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
class StoreTenantIntegrationRequest extends FormRequest
|
||||
{
|
||||
protected ?Integration $integrationModel = null;
|
||||
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function prepareForValidation()
|
||||
{
|
||||
$integrationCode = $this->route('integration_code');
|
||||
$this->integrationModel = Integration::where('integration_code', $integrationCode)->first();
|
||||
|
||||
if (!$this->integrationModel) {
|
||||
throw ValidationException::withMessages([
|
||||
'integration_code' => 'Integration not found.'
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
$rules = [];
|
||||
|
||||
// Dynamic validation rules based on the integration data schema
|
||||
if ($this->integrationModel && $this->integrationModel->integration_data_schema) {
|
||||
foreach ($this->integrationModel->integration_data_schema as $field => $rule) {
|
||||
$rules['integration_data.' . $field] = $rule;
|
||||
}
|
||||
}
|
||||
|
||||
return $rules;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Integration\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class UpdateIntegrationRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
$integration = $this->route('integration');
|
||||
|
||||
return [
|
||||
'name' => ['sometimes', 'required', 'string', 'max:255'],
|
||||
'url' => ['nullable', 'url', 'max:255'],
|
||||
'integration_data_schema' => ['nullable', 'array'],
|
||||
// the code shouldn't ideally be updatable, but if it is:
|
||||
'integration_code' => ['sometimes', 'required', 'string', 'unique:integrations,integration_code,' . ($integration->id ?? '')],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Integration\Services;
|
||||
|
||||
use App\Domains\Integration\Models\Integration;
|
||||
use App\Domains\Integration\Models\TenantIntegration;
|
||||
|
||||
class TenantIntegrationService
|
||||
{
|
||||
public function getTenantIntegration(string $tenantCode, string $integrationCode): ?TenantIntegration
|
||||
{
|
||||
return TenantIntegration::where('tenant_code', $tenantCode)
|
||||
->where('integration_code', $integrationCode)
|
||||
->first();
|
||||
}
|
||||
|
||||
public function getAllForTenant(string $tenantCode)
|
||||
{
|
||||
return TenantIntegration::with('integration')
|
||||
->where('tenant_code', $tenantCode)
|
||||
->get();
|
||||
}
|
||||
|
||||
public function updateOrCreateIntegration(string $tenantCode, Integration $integration, array $data): TenantIntegration
|
||||
{
|
||||
return TenantIntegration::updateOrCreate(
|
||||
[
|
||||
'tenant_code' => $tenantCode,
|
||||
'integration_code' => $integration->integration_code,
|
||||
],
|
||||
[
|
||||
'integration_data' => $data,
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
19
app/Domains/Integration/routes/api.php
Normal file
19
app/Domains/Integration/routes/api.php
Normal file
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
use App\Domains\Integration\Controllers\IntegrationController;
|
||||
use App\Domains\Integration\Controllers\TenantIntegrationController;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
Route::group(['prefix' => 'integrations'], function () {
|
||||
Route::get('/', [IntegrationController::class, 'index']);
|
||||
Route::post('/', [IntegrationController::class, 'store']);
|
||||
Route::get('/{integration}', [IntegrationController::class, 'show']);
|
||||
Route::put('/{integration}', [IntegrationController::class, 'update']);
|
||||
Route::delete('/{integration}', [IntegrationController::class, 'destroy']);
|
||||
});
|
||||
|
||||
Route::group(['prefix' => '{tenant_code}/integrations'], function () {
|
||||
Route::get('/', [TenantIntegrationController::class, 'index']);
|
||||
Route::get('/{integration_code}', [TenantIntegrationController::class, 'show']);
|
||||
Route::post('/{integration_code}', [TenantIntegrationController::class, 'store']);
|
||||
});
|
||||
@@ -10,7 +10,7 @@ use App\Domains\Tenant\Models\Tenant;
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use App\Domains\Purchase\Services\CheckoutService;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
|
||||
@@ -28,28 +28,15 @@ class PurchaseController extends Controller
|
||||
)->response();
|
||||
}
|
||||
|
||||
public function store(StorePurchaseRequest $request, Tenant $tenant): JsonResponse
|
||||
public function store(StorePurchaseRequest $request, Tenant $tenant, CheckoutService $checkoutService): JsonResponse
|
||||
{
|
||||
$data = $request->validated();
|
||||
$items = $data['items'];
|
||||
|
||||
unset($data['items']);
|
||||
|
||||
$variants = $this->resolveTenantVariants($tenant, $items);
|
||||
$purchaseItems = $this->buildPurchaseItemsPayload($items, $variants);
|
||||
|
||||
$purchase = DB::transaction(function () use ($request, $tenant, $data, $purchaseItems): Purchase {
|
||||
/** @var Purchase $purchase */
|
||||
$purchase = Purchase::query()->create([
|
||||
...$data,
|
||||
'tenant_codigo' => $tenant->codigo,
|
||||
'user_id' => $request->user()->id,
|
||||
]);
|
||||
|
||||
$purchase->items()->createMany($purchaseItems);
|
||||
|
||||
return $purchase->load(['items.variant.product', 'items.variant.definitions.productAttribute.attribute']);
|
||||
});
|
||||
$purchase = $checkoutService->processCheckout(
|
||||
$tenant,
|
||||
$request->user()->id,
|
||||
$data
|
||||
);
|
||||
|
||||
return PurchaseResource::make($purchase)->response()->setStatusCode(201);
|
||||
}
|
||||
@@ -63,60 +50,7 @@ class PurchaseController extends Controller
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, array<string, mixed>> $items
|
||||
* @return \Illuminate\Support\Collection<int, ProductVariant>
|
||||
*/
|
||||
protected function resolveTenantVariants(Tenant $tenant, array $items)
|
||||
{
|
||||
$variantIds = collect($items)
|
||||
->pluck('producto_variante_id')
|
||||
->filter()
|
||||
->map(static fn (mixed $id): int => (int) $id)
|
||||
->unique()
|
||||
->values();
|
||||
|
||||
$variants = ProductVariant::query()
|
||||
->with('product')
|
||||
->whereIn('id', $variantIds)
|
||||
->whereHas('product', fn ($query) => $query->where('tenant_codigo', $tenant->codigo))
|
||||
->get()
|
||||
->keyBy('id');
|
||||
|
||||
if ($variants->count() !== $variantIds->count()) {
|
||||
throw ValidationException::withMessages([
|
||||
'items' => 'One or more product variants do not belong to the tenant.',
|
||||
]);
|
||||
}
|
||||
|
||||
return $variants;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, array<string, mixed>> $items
|
||||
* @param \Illuminate\Support\Collection<int, ProductVariant> $variants
|
||||
* @return array<int, array<string, mixed>>
|
||||
*/
|
||||
protected function buildPurchaseItemsPayload(array $items, $variants): array
|
||||
{
|
||||
return collect($items)
|
||||
->map(function (array $item) use ($variants): array {
|
||||
/** @var ProductVariant $variant */
|
||||
$variant = $variants->get((int) $item['producto_variante_id']);
|
||||
$quantity = (int) $item['cantidad'];
|
||||
$unitPrice = (float) ($variant->product?->precio ?? 0);
|
||||
|
||||
return [
|
||||
'producto_variante_id' => $variant->getKey(),
|
||||
'cantidad' => $quantity,
|
||||
'precio_unitario' => $unitPrice,
|
||||
'discount_total' => null,
|
||||
'tax_total' => null,
|
||||
'total' => $unitPrice * $quantity,
|
||||
];
|
||||
})
|
||||
->all();
|
||||
}
|
||||
|
||||
protected function resolveScopedPurchase(Tenant $tenant, int $userId, Purchase $purchase): Purchase
|
||||
{
|
||||
|
||||
@@ -16,6 +16,10 @@ use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
'status',
|
||||
'payment_status',
|
||||
'payment_method',
|
||||
'dni',
|
||||
'telefono',
|
||||
'nombre_apellido',
|
||||
'email',
|
||||
])]
|
||||
class Purchase extends Model
|
||||
{
|
||||
|
||||
@@ -20,7 +20,11 @@ class StorePurchaseRequest extends FormRequest
|
||||
return [
|
||||
'status' => ['sometimes', 'string', Rule::in(['pending', 'paid', 'cancelled'])],
|
||||
'payment_status' => ['sometimes', 'string', Rule::in(['pending', 'approved', 'rejected'])],
|
||||
'payment_method' => ['nullable', 'string', 'max:255'],
|
||||
'payment_method' => ['required', 'string'],
|
||||
'dni' => ['required', 'string'],
|
||||
'telefono' => ['required', 'string'],
|
||||
'nombre_apellido' => ['required', 'string'],
|
||||
'email' => ['required', 'string', 'email'],
|
||||
'items' => ['required', 'array', 'min:1'],
|
||||
'items.*.producto_variante_id' => ['required', 'integer', 'exists:productos_variantes,id'],
|
||||
'items.*.cantidad' => ['required', 'integer', 'min:1'],
|
||||
|
||||
@@ -36,6 +36,10 @@ class PurchaseResource extends JsonResource
|
||||
'status' => $this->status,
|
||||
'payment_status' => $this->payment_status,
|
||||
'payment_method' => $this->payment_method,
|
||||
'dni' => $this->dni,
|
||||
'telefono' => $this->telefono,
|
||||
'nombre_apellido' => $this->nombre_apellido,
|
||||
'email' => $this->email,
|
||||
'items' => PurchaseItemResource::collection($items),
|
||||
'subtotal' => $this->formatMoney($subtotal),
|
||||
'total' => $this->formatMoney($total),
|
||||
|
||||
101
app/Domains/Purchase/Services/CheckoutService.php
Normal file
101
app/Domains/Purchase/Services/CheckoutService.php
Normal file
@@ -0,0 +1,101 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Purchase\Services;
|
||||
|
||||
use App\Domains\Catalog\Models\ProductVariant;
|
||||
use App\Domains\Purchase\Models\Purchase;
|
||||
use App\Domains\Tenant\Models\Tenant;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
class CheckoutService
|
||||
{
|
||||
public function processCheckout(Tenant $tenant, int $userId, array $purchaseData): Purchase
|
||||
{
|
||||
// 1. Preparar items de la compra
|
||||
$items = $purchaseData['items'];
|
||||
unset($purchaseData['items']);
|
||||
|
||||
$variants = $this->resolveTenantVariants($tenant, $items);
|
||||
$purchaseItemsPayload = $this->buildPurchaseItemsPayload($items, $variants);
|
||||
|
||||
// 2. Crear la orden de compra y vaciar carrito
|
||||
return DB::transaction(function () use ($tenant, $userId, $purchaseData, $purchaseItemsPayload): Purchase {
|
||||
/** @var Purchase $purchase */
|
||||
$purchase = Purchase::query()->create([
|
||||
...$purchaseData,
|
||||
'tenant_codigo' => $tenant->codigo,
|
||||
'user_id' => $userId,
|
||||
]);
|
||||
|
||||
$purchase->items()->createMany($purchaseItemsPayload);
|
||||
|
||||
// Vaciar y eliminar el carrito activo del usuario
|
||||
$cart = \App\Domains\Cart\Models\Cart::query()
|
||||
->where('tenant_codigo', $tenant->codigo)
|
||||
->where('user_id', $userId)
|
||||
->first();
|
||||
if ($cart) {
|
||||
$cart->items()->delete();
|
||||
$cart->delete();
|
||||
}
|
||||
|
||||
return $purchase->load(['items.variant.product', 'items.variant.definitions.productAttribute.attribute']);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, array<string, mixed>> $items
|
||||
* @return \Illuminate\Support\Collection<int, ProductVariant>
|
||||
*/
|
||||
protected function resolveTenantVariants(Tenant $tenant, array $items)
|
||||
{
|
||||
$variantIds = collect($items)
|
||||
->pluck('producto_variante_id')
|
||||
->filter()
|
||||
->map(static fn (mixed $id): int => (int) $id)
|
||||
->unique()
|
||||
->values();
|
||||
|
||||
$variants = ProductVariant::query()
|
||||
->with('product')
|
||||
->whereIn('id', $variantIds)
|
||||
->whereHas('product', fn ($query) => $query->where('tenant_codigo', $tenant->codigo))
|
||||
->get()
|
||||
->keyBy('id');
|
||||
|
||||
if ($variants->count() !== $variantIds->count()) {
|
||||
throw ValidationException::withMessages([
|
||||
'items' => 'One or more product variants do not belong to the tenant.',
|
||||
]);
|
||||
}
|
||||
|
||||
return $variants;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, array<string, mixed>> $items
|
||||
* @param \Illuminate\Support\Collection<int, ProductVariant> $variants
|
||||
* @return array<int, array<string, mixed>>
|
||||
*/
|
||||
protected function buildPurchaseItemsPayload(array $items, $variants): array
|
||||
{
|
||||
return collect($items)
|
||||
->map(function (array $item) use ($variants): array {
|
||||
/** @var ProductVariant $variant */
|
||||
$variant = $variants->get((int) $item['producto_variante_id']);
|
||||
$quantity = (int) $item['cantidad'];
|
||||
$unitPrice = (float) ($variant->product?->precio ?? 0);
|
||||
|
||||
return [
|
||||
'producto_variante_id' => $variant->getKey(),
|
||||
'cantidad' => $quantity,
|
||||
'precio_unitario' => $unitPrice,
|
||||
'discount_total' => null,
|
||||
'tax_total' => null,
|
||||
'total' => $unitPrice * $quantity,
|
||||
];
|
||||
})
|
||||
->all();
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Domains\Tenant\Models;
|
||||
|
||||
use App\Domains\Attachable\Models\Attachment;
|
||||
use App\Domains\BankAccount\Models\BankAccount;
|
||||
use App\Domains\Catalog\Models\Product;
|
||||
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
@@ -55,5 +56,21 @@ class Tenant extends Model
|
||||
{
|
||||
return $this->hasMany(Product::class, 'tenant_codigo', 'codigo');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return HasMany<BankAccount, $this>
|
||||
*/
|
||||
public function bankAccounts(): HasMany
|
||||
{
|
||||
return $this->hasMany(BankAccount::class, 'tenant_code', 'codigo');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return BelongsTo<BankAccount, $this>
|
||||
*/
|
||||
public function selectedBankAccount(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(BankAccount::class, 'selected_bank_account_id');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -35,4 +35,8 @@ return [
|
||||
],
|
||||
],
|
||||
|
||||
'integrations' => [
|
||||
'secret' => env('INTEGRATION_SECRET'),
|
||||
],
|
||||
|
||||
];
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('integrations', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('integration_code')->unique();
|
||||
$table->string('name');
|
||||
$table->string('url')->nullable();
|
||||
$table->json('integration_data_schema')->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('integrations');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('tenant_integration', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('integration_code');
|
||||
$table->string('tenant_code');
|
||||
$table->longText('integration_data')->nullable(); // Stored as longText because it's encrypted JSON
|
||||
$table->timestamps();
|
||||
|
||||
// Foreign keys
|
||||
$table->foreign('integration_code')->references('integration_code')->on('integrations')->onDelete('cascade');
|
||||
$table->foreign('tenant_code')->references('codigo')->on('tenants')->onDelete('cascade');
|
||||
|
||||
$table->unique(['integration_code', 'tenant_code']);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('tenant_integration');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('bank_accounts', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('tenant_code');
|
||||
$table->string('titular');
|
||||
$table->string('entidad');
|
||||
$table->string('alias');
|
||||
$table->string('cvu');
|
||||
$table->timestamps();
|
||||
|
||||
$table->foreign('tenant_code')
|
||||
->references('codigo')
|
||||
->on('tenants')
|
||||
->cascadeOnUpdate()
|
||||
->cascadeOnDelete();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('bank_accounts');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('tenants', function (Blueprint $table) {
|
||||
$table->unsignedBigInteger('selected_bank_account_id')->nullable()->after('footer_logo_id');
|
||||
|
||||
$table->foreign('selected_bank_account_id')
|
||||
->references('id')
|
||||
->on('bank_accounts')
|
||||
->nullOnDelete();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('tenants', function (Blueprint $table) {
|
||||
$table->dropForeign(['selected_bank_account_id']);
|
||||
$table->dropColumn('selected_bank_account_id');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->string('dni')->nullable()->after('email');
|
||||
$table->string('telefono')->nullable()->after('dni');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->dropColumn(['dni', 'telefono']);
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('compras', function (Blueprint $table) {
|
||||
$table->string('dni')->nullable()->after('payment_method');
|
||||
$table->string('telefono')->nullable()->after('dni');
|
||||
$table->string('nombre_apellido')->nullable()->after('telefono');
|
||||
$table->string('email')->nullable()->after('nombre_apellido');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('compras', function (Blueprint $table) {
|
||||
$table->dropColumn(['dni', 'telefono', 'nombre_apellido', 'email']);
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -28,6 +28,7 @@ class DatabaseSeeder extends Seeder
|
||||
CategorySeeder::class,
|
||||
BrandSeeder::class,
|
||||
ProductCatalogFromImagesSeeder::class,
|
||||
TelepagosIntegrationSeeder::class,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
39
database/seeders/TelepagosIntegrationSeeder.php
Normal file
39
database/seeders/TelepagosIntegrationSeeder.php
Normal file
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use App\Domains\Integration\Models\Integration;
|
||||
use Illuminate\Database\Seeder;
|
||||
|
||||
class TelepagosIntegrationSeeder extends Seeder
|
||||
{
|
||||
/**
|
||||
* Run the database seeds.
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
Integration::updateOrCreate(
|
||||
['integration_code' => 'telepagos'],
|
||||
[
|
||||
'name' => 'Telepagos',
|
||||
'url' => 'https://api.telepagos.com.ar',
|
||||
'integration_data_schema' => [
|
||||
'username' => 'required|string',
|
||||
'password' => 'required|string',
|
||||
]
|
||||
]
|
||||
);
|
||||
|
||||
Integration::updateOrCreate(
|
||||
['integration_code' => 'telepagos_homo'],
|
||||
[
|
||||
'name' => 'Telepagos Homologación',
|
||||
'url' => 'https://api.homo.telepagos.com.ar',
|
||||
'integration_data_schema' => [
|
||||
'username' => 'required|string',
|
||||
'password' => 'required|string',
|
||||
]
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -72,7 +72,7 @@ class TenantSeeder extends Seeder
|
||||
true
|
||||
);
|
||||
|
||||
$this->tenantService->create([
|
||||
$tenant = $this->tenantService->create([
|
||||
'codigo' => 'sonder',
|
||||
'nombre' => 'Sonder',
|
||||
'dominio' => 'localhost',
|
||||
@@ -85,5 +85,32 @@ class TenantSeeder extends Seeder
|
||||
'header_logo' => $headerLogo,
|
||||
'footer_logo' => $footerLogo,
|
||||
]);
|
||||
|
||||
$bankAccount1 = \App\Domains\BankAccount\Models\BankAccount::query()->create([
|
||||
'tenant_code' => $tenant->codigo,
|
||||
'titular' => 'Sonder S.A. (Principal)',
|
||||
'entidad' => 'Banco de la Nación Argentina',
|
||||
'alias' => 'sonder.indumentaria',
|
||||
'cvu' => '0110065420006540987654',
|
||||
]);
|
||||
|
||||
\App\Domains\BankAccount\Models\BankAccount::query()->create([
|
||||
'tenant_code' => $tenant->codigo,
|
||||
'titular' => 'Sonder S.A. (Secundaria)',
|
||||
'entidad' => 'Banco de Galicia y Buenos Aires',
|
||||
'alias' => 'sonder.indumentaria.galicia',
|
||||
'cvu' => '0070012345678901234567',
|
||||
]);
|
||||
|
||||
\App\Domains\BankAccount\Models\BankAccount::query()->create([
|
||||
'tenant_code' => $tenant->codigo,
|
||||
'titular' => 'Sonder S.A. (Mercado Pago)',
|
||||
'entidad' => 'Mercado Pago',
|
||||
'alias' => 'sonder.indumentaria.mp',
|
||||
'cvu' => '0000003100012345678901',
|
||||
]);
|
||||
|
||||
$tenant->selected_bank_account_id = $bankAccount1->id;
|
||||
$tenant->save();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,3 +9,5 @@ require __DIR__.'/../app/Domains/Cart/routes/api.php';
|
||||
require __DIR__.'/../app/Domains/StorageTest/routes/api.php';
|
||||
require __DIR__.'/../app/Domains/Purchase/routes/api.php';
|
||||
require __DIR__.'/../app/Domains/Tenant/routes/api.php';
|
||||
require __DIR__.'/../app/Domains/BankAccount/routes/api.php';
|
||||
require __DIR__.'/../app/Domains/Integration/routes/api.php';
|
||||
|
||||
@@ -36,7 +36,7 @@ class LoginControllerTest extends TestCase
|
||||
$this->assertNotEmpty($response->json('token'));
|
||||
|
||||
$this->withHeader('Authorization', 'Bearer '.$response->json('token'))
|
||||
->getJson('/api/user')
|
||||
->getJson('/api/me')
|
||||
->assertOk()
|
||||
->assertJsonPath('id', $user->id)
|
||||
->assertJsonPath('email', 'grace@example.com');
|
||||
@@ -44,7 +44,7 @@ class LoginControllerTest extends TestCase
|
||||
|
||||
public function test_it_rejects_access_to_the_current_user_endpoint_without_a_token(): void
|
||||
{
|
||||
$this->getJson('/api/user')->assertUnauthorized();
|
||||
$this->getJson('/api/me')->assertUnauthorized();
|
||||
}
|
||||
|
||||
public function test_it_logs_out_the_current_token(): void
|
||||
|
||||
@@ -17,17 +17,23 @@ class RegisterControllerTest extends TestCase
|
||||
'email' => 'ada@example.com',
|
||||
'password' => 'secret123',
|
||||
'password_confirmation' => 'secret123',
|
||||
'dni' => '12345678A',
|
||||
'telefono' => '+541122334455',
|
||||
]);
|
||||
|
||||
$response
|
||||
->assertCreated()
|
||||
->assertJsonPath('message', 'Usuario registrado correctamente.')
|
||||
->assertJsonPath('data.nombre_apellido', 'Ada Lovelace')
|
||||
->assertJsonPath('data.email', 'ada@example.com');
|
||||
->assertJsonPath('data.email', 'ada@example.com')
|
||||
->assertJsonPath('data.dni', '12345678A')
|
||||
->assertJsonPath('data.telefono', '+541122334455');
|
||||
|
||||
$this->assertDatabaseHas('users', [
|
||||
'nombre_apellido' => 'Ada Lovelace',
|
||||
'email' => 'ada@example.com',
|
||||
'dni' => '12345678A',
|
||||
'telefono' => '+541122334455',
|
||||
]);
|
||||
|
||||
$user = User::query()->where('email', 'ada@example.com')->firstOrFail();
|
||||
@@ -35,6 +41,31 @@ class RegisterControllerTest extends TestCase
|
||||
$this->assertNotSame('secret123', $user->password);
|
||||
}
|
||||
|
||||
public function test_it_registers_a_user_without_optional_fields(): void
|
||||
{
|
||||
$response = $this->postJson('/api/register', [
|
||||
'nombre_apellido' => 'Alan Turing',
|
||||
'email' => 'alan@example.com',
|
||||
'password' => 'secret123',
|
||||
'password_confirmation' => 'secret123',
|
||||
]);
|
||||
|
||||
$response
|
||||
->assertCreated()
|
||||
->assertJsonPath('message', 'Usuario registrado correctamente.')
|
||||
->assertJsonPath('data.nombre_apellido', 'Alan Turing')
|
||||
->assertJsonPath('data.email', 'alan@example.com')
|
||||
->assertJsonPath('data.dni', null)
|
||||
->assertJsonPath('data.telefono', null);
|
||||
|
||||
$this->assertDatabaseHas('users', [
|
||||
'nombre_apellido' => 'Alan Turing',
|
||||
'email' => 'alan@example.com',
|
||||
'dni' => null,
|
||||
'telefono' => null,
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_it_validates_required_fields_and_unique_email(): void
|
||||
{
|
||||
User::factory()->create([
|
||||
|
||||
140
tests/Feature/Purchase/StorePurchaseTest.php
Normal file
140
tests/Feature/Purchase/StorePurchaseTest.php
Normal file
@@ -0,0 +1,140 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Purchase;
|
||||
|
||||
use App\Domains\Auth\Models\User;
|
||||
use App\Domains\Catalog\Models\Product;
|
||||
use App\Domains\Catalog\Models\ProductVariant;
|
||||
use App\Domains\Tenant\Models\Tenant;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class StorePurchaseTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_it_creates_a_purchase_with_customer_details_and_clears_the_cart(): void
|
||||
{
|
||||
// 1. Setup Tenant
|
||||
$tenant = $this->createTenant('sonder', 'Sonder', 'sonder.com.ar');
|
||||
|
||||
// 2. Setup User
|
||||
$user = User::factory()->create([
|
||||
'email' => 'buyer@example.com'
|
||||
]);
|
||||
|
||||
// 3. Setup Product & Variant
|
||||
$category = \App\Domains\Catalog\Models\Category::query()->create([
|
||||
'tenant_code' => 'sonder',
|
||||
'nombre' => 'Test Category',
|
||||
]);
|
||||
|
||||
$product = Product::query()->create([
|
||||
'tenant_codigo' => 'sonder',
|
||||
'categoria_id' => $category->id,
|
||||
'slug' => 'test-product',
|
||||
'nombre' => 'Test Product',
|
||||
'descripcion' => 'Test',
|
||||
'precio' => '50.00',
|
||||
]);
|
||||
|
||||
$variant = ProductVariant::query()->create([
|
||||
'producto_id' => $product->id,
|
||||
'slug' => 'test-variant',
|
||||
'nombre' => 'Test Variant',
|
||||
'stock' => 10,
|
||||
'precio' => '50.00',
|
||||
]);
|
||||
|
||||
// 4. Add item to user's cart
|
||||
$this->actingAs($user, 'sanctum')
|
||||
->postJson('/api/tenants/sonder/cart/items', [
|
||||
'product_variant_id' => $variant->id,
|
||||
'cantidad' => 2,
|
||||
])
|
||||
->assertOk();
|
||||
|
||||
// Check cart exists in DB
|
||||
$this->assertDatabaseHas('carritos', [
|
||||
'tenant_codigo' => 'sonder',
|
||||
'user_id' => $user->id,
|
||||
]);
|
||||
|
||||
// 5. Submit Purchase with payment_method at root
|
||||
$response = $this->actingAs($user, 'sanctum')
|
||||
->postJson('/api/tenants/sonder/compras', [
|
||||
'dni' => '987654321',
|
||||
'telefono' => '+54 9 341 555-4321',
|
||||
'nombre_apellido' => 'Juan Perez',
|
||||
'email' => 'juan.perez@example.com',
|
||||
'payment_method' => 'transferencia',
|
||||
'items' => [
|
||||
[
|
||||
'producto_variante_id' => $variant->id,
|
||||
'cantidad' => 2,
|
||||
]
|
||||
]
|
||||
]);
|
||||
|
||||
// 6. Assertions
|
||||
$response->assertCreated();
|
||||
$response->assertJsonPath('data.dni', '987654321');
|
||||
$response->assertJsonPath('data.telefono', '+54 9 341 555-4321');
|
||||
$response->assertJsonPath('data.nombre_apellido', 'Juan Perez');
|
||||
$response->assertJsonPath('data.email', 'juan.perez@example.com');
|
||||
$response->assertJsonPath('data.payment_method', 'transferencia');
|
||||
$response->assertJsonPath('data.tenant_codigo', 'sonder');
|
||||
|
||||
// Assert Purchase saved in DB
|
||||
$this->assertDatabaseHas('compras', [
|
||||
'tenant_codigo' => 'sonder',
|
||||
'user_id' => $user->id,
|
||||
'dni' => '987654321',
|
||||
'telefono' => '+54 9 341 555-4321',
|
||||
'nombre_apellido' => 'Juan Perez',
|
||||
'email' => 'juan.perez@example.com',
|
||||
'payment_method' => 'transferencia',
|
||||
]);
|
||||
|
||||
// Assert Cart was cleared (deleted)
|
||||
$this->assertDatabaseMissing('carritos', [
|
||||
'tenant_codigo' => 'sonder',
|
||||
'user_id' => $user->id,
|
||||
]);
|
||||
}
|
||||
|
||||
protected function createTenant(string $codigo, string $nombre, string $dominio): Tenant
|
||||
{
|
||||
$hdrKey = (string) \Illuminate\Support\Str::uuid();
|
||||
$ftrKey = (string) \Illuminate\Support\Str::uuid();
|
||||
|
||||
$headerAttachment = \App\Domains\Attachable\Models\Attachment::create([
|
||||
'key' => $hdrKey,
|
||||
'path' => 'tenants/' . $hdrKey . '.png',
|
||||
'filename' => 'logo_header.png',
|
||||
'type' => \App\Domains\Attachable\Enums\AttachmentType::Image,
|
||||
'mime_type' => 'image/png',
|
||||
]);
|
||||
$footerAttachment = \App\Domains\Attachable\Models\Attachment::create([
|
||||
'key' => $ftrKey,
|
||||
'path' => 'tenants/' . $ftrKey . '.png',
|
||||
'filename' => 'logo_footer.png',
|
||||
'type' => \App\Domains\Attachable\Enums\AttachmentType::Image,
|
||||
'mime_type' => 'image/png',
|
||||
]);
|
||||
|
||||
return Tenant::create([
|
||||
'codigo' => $codigo,
|
||||
'nombre' => $nombre,
|
||||
'dominio' => $dominio,
|
||||
'primary_color' => '#111111',
|
||||
'secondary_color' => '#222222',
|
||||
'danger_color' => '#333333',
|
||||
'success_color' => '#28a745',
|
||||
'header_bg_color' => '#444444',
|
||||
'footer_bg_color' => '#444444',
|
||||
'header_logo_id' => $headerAttachment->id,
|
||||
'footer_logo_id' => $footerAttachment->id,
|
||||
]);
|
||||
}
|
||||
}
|
||||
242
tests/Feature/Tenant/BankAccountControllerTest.php
Normal file
242
tests/Feature/Tenant/BankAccountControllerTest.php
Normal file
@@ -0,0 +1,242 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Tenant;
|
||||
|
||||
use App\Domains\BankAccount\Models\BankAccount;
|
||||
use App\Domains\Tenant\Models\Tenant;
|
||||
use App\Domains\Auth\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class BankAccountControllerTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
private Tenant $tenant;
|
||||
private User $user;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$hdrKey = (string) \Illuminate\Support\Str::uuid();
|
||||
$ftrKey = (string) \Illuminate\Support\Str::uuid();
|
||||
|
||||
$headerAttachment = \App\Domains\Attachable\Models\Attachment::create([
|
||||
'key' => $hdrKey,
|
||||
'path' => 'tenants/' . $hdrKey . '.png',
|
||||
'filename' => 'logo_header.png',
|
||||
'type' => \App\Domains\Attachable\Enums\AttachmentType::Image,
|
||||
'mime_type' => 'image/png',
|
||||
]);
|
||||
$footerAttachment = \App\Domains\Attachable\Models\Attachment::create([
|
||||
'key' => $ftrKey,
|
||||
'path' => 'tenants/' . $ftrKey . '.png',
|
||||
'filename' => 'logo_footer.png',
|
||||
'type' => \App\Domains\Attachable\Enums\AttachmentType::Image,
|
||||
'mime_type' => 'image/png',
|
||||
]);
|
||||
|
||||
$this->tenant = Tenant::create([
|
||||
'codigo' => 'acme',
|
||||
'nombre' => 'Acme',
|
||||
'dominio' => 'acme.com',
|
||||
'primary_color' => '#ff0000',
|
||||
'secondary_color' => '#00ff00',
|
||||
'danger_color' => '#0000ff',
|
||||
'success_color' => '#00ff00',
|
||||
'header_bg_color' => '#ffffff',
|
||||
'footer_bg_color' => '#ffffff',
|
||||
'header_logo_id' => $headerAttachment->id,
|
||||
'footer_logo_id' => $footerAttachment->id,
|
||||
]);
|
||||
|
||||
$this->user = User::factory()->create();
|
||||
$this->actingAs($this->user, 'sanctum');
|
||||
}
|
||||
|
||||
public function test_it_lists_bank_accounts_for_a_tenant(): void
|
||||
{
|
||||
BankAccount::query()->create([
|
||||
'tenant_code' => 'acme',
|
||||
'titular' => 'John Doe',
|
||||
'entidad' => 'Banco Galicia',
|
||||
'alias' => 'john.doe.galicia',
|
||||
'cvu' => '1234567890123456789012',
|
||||
]);
|
||||
|
||||
$response = $this->getJson("/api/tenants/acme/bank-accounts");
|
||||
|
||||
$response->assertOk()
|
||||
->assertJsonCount(1, 'data')
|
||||
->assertJsonPath('data.0.titular', 'John Doe');
|
||||
}
|
||||
|
||||
public function test_it_creates_a_bank_account_for_a_tenant(): void
|
||||
{
|
||||
$response = $this->postJson("/api/tenants/acme/bank-accounts", [
|
||||
'titular' => 'Jane Doe',
|
||||
'entidad' => 'Banco Nación',
|
||||
'alias' => 'jane.doe.nacion',
|
||||
'cvu' => '9876543210987654321098',
|
||||
]);
|
||||
|
||||
$response->assertCreated()
|
||||
->assertJsonPath('data.titular', 'Jane Doe');
|
||||
|
||||
$this->assertDatabaseHas('bank_accounts', [
|
||||
'tenant_code' => 'acme',
|
||||
'titular' => 'Jane Doe',
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_it_shows_a_bank_account(): void
|
||||
{
|
||||
$account = BankAccount::query()->create([
|
||||
'tenant_code' => 'acme',
|
||||
'titular' => 'John Doe',
|
||||
'entidad' => 'Banco Galicia',
|
||||
'alias' => 'john.doe.galicia',
|
||||
'cvu' => '1234567890123456789012',
|
||||
]);
|
||||
|
||||
$response = $this->getJson("/api/tenants/acme/bank-accounts/{$account->id}");
|
||||
|
||||
$response->assertOk()
|
||||
->assertJsonPath('data.titular', 'John Doe');
|
||||
}
|
||||
|
||||
public function test_it_updates_a_bank_account(): void
|
||||
{
|
||||
$account = BankAccount::query()->create([
|
||||
'tenant_code' => 'acme',
|
||||
'titular' => 'John Doe',
|
||||
'entidad' => 'Banco Galicia',
|
||||
'alias' => 'john.doe.galicia',
|
||||
'cvu' => '1234567890123456789012',
|
||||
]);
|
||||
|
||||
$response = $this->putJson("/api/tenants/acme/bank-accounts/{$account->id}", [
|
||||
'titular' => 'John Doe Updated',
|
||||
]);
|
||||
|
||||
$response->assertOk()
|
||||
->assertJsonPath('data.titular', 'John Doe Updated');
|
||||
|
||||
$this->assertDatabaseHas('bank_accounts', [
|
||||
'id' => $account->id,
|
||||
'titular' => 'John Doe Updated',
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_it_deletes_a_bank_account(): void
|
||||
{
|
||||
$account = BankAccount::query()->create([
|
||||
'tenant_code' => 'acme',
|
||||
'titular' => 'John Doe',
|
||||
'entidad' => 'Banco Galicia',
|
||||
'alias' => 'john.doe.galicia',
|
||||
'cvu' => '1234567890123456789012',
|
||||
]);
|
||||
|
||||
$response = $this->deleteJson("/api/tenants/acme/bank-accounts/{$account->id}");
|
||||
|
||||
$response->assertNoContent();
|
||||
|
||||
$this->assertDatabaseMissing('bank_accounts', ['id' => $account->id]);
|
||||
}
|
||||
|
||||
public function test_it_selects_a_bank_account_for_a_tenant(): void
|
||||
{
|
||||
$account = BankAccount::query()->create([
|
||||
'tenant_code' => 'acme',
|
||||
'titular' => 'John Doe',
|
||||
'entidad' => 'Banco Galicia',
|
||||
'alias' => 'john.doe.galicia',
|
||||
'cvu' => '1234567890123456789012',
|
||||
]);
|
||||
|
||||
$response = $this->postJson("/api/tenants/acme/bank-accounts/{$account->id}/select");
|
||||
|
||||
$response->assertOk()
|
||||
->assertJsonPath('data.selected_bank_account_id', $account->id);
|
||||
|
||||
$this->tenant->refresh();
|
||||
$this->assertEquals($account->id, $this->tenant->selected_bank_account_id);
|
||||
}
|
||||
|
||||
public function test_it_deletes_related_bank_accounts_when_tenant_is_deleted(): void
|
||||
{
|
||||
$account = BankAccount::query()->create([
|
||||
'tenant_code' => 'acme',
|
||||
'titular' => 'John Doe',
|
||||
'entidad' => 'Banco Galicia',
|
||||
'alias' => 'john.doe.galicia',
|
||||
'cvu' => '1234567890123456789012',
|
||||
]);
|
||||
|
||||
$this->assertDatabaseHas('bank_accounts', ['id' => $account->id]);
|
||||
|
||||
$this->tenant->delete();
|
||||
|
||||
$this->assertDatabaseMissing('bank_accounts', ['id' => $account->id]);
|
||||
}
|
||||
|
||||
public function test_it_sets_selected_bank_account_id_to_null_when_selected_bank_account_is_deleted(): void
|
||||
{
|
||||
$account = BankAccount::query()->create([
|
||||
'tenant_code' => 'acme',
|
||||
'titular' => 'John Doe',
|
||||
'entidad' => 'Banco Galicia',
|
||||
'alias' => 'john.doe.galicia',
|
||||
'cvu' => '1234567890123456789012',
|
||||
]);
|
||||
|
||||
$this->tenant->selected_bank_account_id = $account->id;
|
||||
$this->tenant->save();
|
||||
|
||||
$account->delete();
|
||||
|
||||
$this->tenant->refresh();
|
||||
$this->assertNull($this->tenant->selected_bank_account_id);
|
||||
}
|
||||
|
||||
public function test_it_gets_the_selected_bank_account_for_a_tenant(): void
|
||||
{
|
||||
$account = BankAccount::query()->create([
|
||||
'tenant_code' => 'acme',
|
||||
'titular' => 'John Doe',
|
||||
'entidad' => 'Banco Galicia',
|
||||
'alias' => 'john.doe.galicia',
|
||||
'cvu' => '1234567890123456789012',
|
||||
]);
|
||||
|
||||
// When no bank account is selected
|
||||
$response = $this->getJson("/api/tenants/acme/bank-accounts/selected");
|
||||
$response->assertNotFound();
|
||||
|
||||
// Select the bank account
|
||||
$this->tenant->selected_bank_account_id = $account->id;
|
||||
$this->tenant->save();
|
||||
|
||||
// Get selected bank account
|
||||
$response = $this->getJson("/api/tenants/acme/bank-accounts/selected");
|
||||
$response->assertOk()
|
||||
->assertJsonPath('data.id', $account->id)
|
||||
->assertJsonPath('data.titular', 'John Doe');
|
||||
}
|
||||
|
||||
public function test_it_rejects_unauthenticated_requests(): void
|
||||
{
|
||||
$this->app['auth']->forgetGuards();
|
||||
|
||||
$response = $this->getJson("/api/tenants/acme/bank-accounts/selected");
|
||||
$response->assertStatus(401);
|
||||
|
||||
$response2 = $this->getJson("/api/tenants/acme/bank-accounts");
|
||||
$response2->assertStatus(401);
|
||||
|
||||
$response3 = $this->postJson("/api/tenants/acme/bank-accounts", []);
|
||||
$response3->assertStatus(401);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user