*/ function jsonBody(array $payload): array { $raw = json_encode($payload, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); $raw = preg_replace('/"\{\{([a-z_]+_id)\}\}"/', '{{$1}}', $raw); return [ 'mode' => 'raw', 'raw' => $raw, 'options' => ['raw' => ['language' => 'json']], ]; } /** @param array $fields * @return array */ function formDataBody(array $fields, array $fileFields = []): array { $formData = []; foreach ($fields as $key => $value) { $formData[] = ['key' => $key, 'value' => $value, 'type' => 'text']; } foreach ($fileFields as $key) { $formData[] = ['key' => $key, 'type' => 'file', 'src' => []]; } return ['mode' => 'formdata', 'formdata' => $formData]; } /** @return array|null */ function bodyFor(string $method, string $uri): ?array { $key = $method.' '.$uri; $exact = [ 'POST api/auth/google/exchange' => ['oauth_code' => '{{oauth_code}}', 'tenant_codigo' => '{{tenant_code}}'], 'POST api/register' => ['tenant_codigo' => '{{tenant_code}}', 'nombre_apellido' => 'Usuario Demo', 'email' => '{{user_email}}', 'password' => '{{user_password}}', 'password_confirmation' => '{{user_password}}', 'dni' => '30123456', 'telefono' => '+5491112345678'], 'POST api/login' => ['email' => '{{user_email}}', 'password' => '{{user_password}}', 'tenant_codigo' => '{{tenant_code}}'], 'PUT api/me' => ['nombre_apellido' => 'Usuario Demo Actualizado', 'email' => '{{user_email}}', 'dni' => '30123456', 'telefono' => '+5491112345678'], 'POST api/password/reset-attempts' => ['tenant_codigo' => '{{tenant_code}}', 'email' => '{{user_email}}'], 'POST api/password/reset-attempts/validate' => ['email' => '{{user_email}}', 'codigo' => '{{reset_code}}'], 'POST api/password/reset' => ['email' => '{{user_email}}', 'codigo' => '{{reset_code}}', 'password' => '{{user_password}}', 'password_confirmation' => '{{user_password}}'], 'POST api/clients' => ['code' => 'cliente-demo', 'name' => 'Cliente Demo'], 'PUT api/clients/{client}' => ['code' => 'cliente-demo', 'name' => 'Cliente Demo Actualizado'], 'PATCH api/clients/{client}' => ['name' => 'Cliente Demo Actualizado'], 'POST api/integrations' => ['integration_code' => 'telepagos', 'name' => 'Telepagos', 'url' => 'https://api.example.com', 'integration_data_schema' => ['api_key' => ['required', 'string']], 'requires_client_configuration' => true], 'PUT api/integrations/{integration}' => ['name' => 'Telepagos', 'url' => 'https://api.example.com', 'requires_client_configuration' => true], 'PUT api/clients/{client}/integrations/{integration_code}' => ['integration_data' => ['api_key' => 'replace-me']], 'POST api/menues' => ['code' => 'demo', 'label' => 'Demo', 'parent_menu_code' => null, 'content_type' => 'static', 'static_content_schema' => ['title' => ['required', 'string']], 'route' => '/demo'], 'PUT api/menues/{menue}' => ['label' => 'Demo actualizado', 'route' => '/demo'], 'PATCH api/menues/{menue}' => ['label' => 'Demo actualizado'], 'POST api/{tenant_code}/menues/{menu_code}' => ['static_content' => ['title' => 'Contenido demo']], 'POST api/webhooks/telepagos/{client}' => ['id' => 'payment-id-demo'], 'POST api/{tenant_code}/mail-test/send' => ['to' => 'destinatario@example.com', 'subject' => 'Prueba ShopIt', 'message' => 'Correo de prueba enviado desde Postman.'], 'POST api/tenants/{tenant:codigo}/cart/items' => ['catalog_item_id' => '{{catalog_item_id}}', 'variant_id' => '{{variant_id}}', 'cantidad' => 1], 'PATCH api/tenants/{tenant:codigo}/cart/items/{cartItem}' => ['cantidad' => 2, 'variant_id' => '{{variant_id}}'], 'POST api/tenants/{tenant:codigo}/catalog-items/{catalogItem}/variant-options' => ['selected_values' => ['color' => 'azul'], 'cart_item_id' => '{{cart_item_id}}'], 'POST api/tenants/{tenant:codigo}/compras/start-checkout' => ['cart_id' => '{{cart_id}}'], 'PATCH api/tenants/{tenant:codigo}/compras/{compra}/customer-data' => ['dni' => '30123456', 'telefono' => '+5491112345678', 'nombre_apellido' => 'Usuario Demo', 'email' => '{{user_email}}'], 'PATCH api/tenants/{tenant:codigo}/checkout-carts/{cart}/items/{cartItem}' => ['cantidad' => 2], 'POST api/tenants/{tenant:codigo}/checkout-carts/{cart}/edit' => [], 'POST api/tenants/{tenant:codigo}/compras/{compra}/payment-intent' => ['method' => 'transfer', 'transfer_payer_dni' => '30123456'], 'POST api/tenants/{tenant:codigo}/tickets/pdf' => ['ticket_ids' => [1]], 'POST api/v1/adminapp/login' => ['email' => '{{admin_email}}', 'password' => '{{admin_password}}'], 'POST api/v1/adminapp/password/reset-attempts' => ['email' => '{{admin_email}}'], 'POST api/v1/adminapp/password/reset-attempts/validate' => ['email' => '{{admin_email}}', 'codigo' => '{{reset_code}}'], 'POST api/v1/adminapp/password/reset' => ['email' => '{{admin_email}}', 'codigo' => '{{reset_code}}', 'password' => '{{admin_password}}', 'password_confirmation' => '{{admin_password}}'], 'PUT api/v1/adminapp/tenant/event' => ['title' => 'Evento Demo', 'location' => 'Buenos Aires', 'dates' => [['date' => '2026-12-01', 'start_time' => '18:00', 'end_time' => '23:00']], 'social_media' => [['code' => 'instagram', 'url' => 'https://instagram.com/example', 'orden' => 0]]], 'POST api/v1/adminapp/tenant/featured-groups' => ['category_name' => 'Destacados', 'is_featured' => true], 'PUT api/v1/adminapp/tenant/featured-groups/{featuredGroup}' => ['category_name' => 'Destacados', 'is_featured' => true], 'POST api/v1/adminapp/tenant/staff' => ['nombre_apellido' => 'Operador Demo', 'dni' => '30123456', 'email' => 'operador@example.com', 'category_ids' => [1]], 'PUT api/v1/adminapp/tenant/staff/{staff}' => ['nombre_apellido' => 'Operador Demo', 'dni' => '30123456', 'email' => 'operador@example.com', 'category_ids' => [1]], 'PATCH api/v1/adminapp/tenant/staff/{staff}' => ['nombre_apellido' => 'Operador Demo', 'dni' => '30123456', 'email' => 'operador@example.com', 'category_ids' => [1]], 'PUT api/v1/adminapp/tenant/website-extras/{websiteExtraCode}' => ['enabled' => true, 'content' => ['title' => 'Contenido demo']], 'PATCH api/v1/adminapp/tenant/website-extras/{websiteExtraCode}/toggle' => ['enabled' => true], 'POST api/v1/adminapp/tenant/accommodations' => ['variants' => [['title' => 'Habitación doble', 'description' => 'Dos personas', 'stock' => 10, 'price' => 100000]]], 'POST api/v1/adminapp/tenant/entries' => ['entries' => [['title' => 'Entrada general', 'description' => 'Acceso general', 'event_date_ids' => [1], 'stock' => 100, 'price' => 25000]]], 'POST api/v1/adminapp/tenant/foods' => ['variants' => [['event_date_id' => 1, 'schedule' => '21:00', 'service' => 'Cena', 'description' => 'Menú completo', 'stock' => 100, 'price' => 15000]]], 'POST api/v1/adminapp/tenant/merchandise' => ['items' => [['title' => 'Camiseta', 'description' => 'Camiseta oficial', 'max_units_per_user' => 2, 'variants' => [['color' => 'Azul', 'size' => 'M', 'stock' => 20, 'price' => 30000]]]]], 'PUT api/v1/adminapp/tenant/desfile/entries' => ['rows' => [['type' => 'General', 'sector' => 'A', 'row' => 1, 'max_seat' => 20, 'price' => 25000]]], 'PATCH api/v1/adminapp/tenant/desfile/entries/image' => ['is_enabled' => true], 'POST api/v1/scanner/login' => ['email' => '{{scanner_email}}', 'password' => '{{scanner_password}}'], 'POST api/v1/scanner/password/reset-attempts' => ['email' => '{{scanner_email}}'], 'POST api/v1/scanner/password/reset-attempts/validate' => ['email' => '{{scanner_email}}', 'codigo' => '{{reset_code}}'], 'POST api/v1/scanner/password/reset' => ['email' => '{{scanner_email}}', 'codigo' => '{{reset_code}}', 'password' => '{{scanner_password}}', 'password_confirmation' => '{{scanner_password}}'], ]; if (isset($exact[$key])) { return jsonBody($exact[$key]); } if ($key === 'POST api/tenants') { return jsonBody([ 'client_id' => '{{client_id}}', 'codigo' => '{{tenant_code}}', 'nombre' => 'Tenant Demo', 'dominio' => '{{tenant_domain}}', 'site_title' => 'ShopIt Demo', 'primary_color' => '#111827', 'secondary_color' => '#2563EB', 'danger_color' => '#DC2626', 'success_color' => '#16A34A', 'header_bg_color' => '#FFFFFF', 'footer_bg_color' => '#111827', 'header_logo' => TINY_PNG, 'footer_logo' => TINY_PNG, 'search_product_layout' => 'column_with_image', 'search_group_layout' => 'paginated', 'search_items_per_page' => 12, 'display_categories' => true, 'display_seach_bar' => true, 'display_cart' => true, 'cart_editing_enabled' => true, 'website_type_code' => 'shopit', ]); } if (in_array($key, ['PUT api/tenants/{tenant}', 'PATCH api/tenants/{tenant}'], true)) { return jsonBody(['nombre' => 'Tenant Demo Actualizado', 'site_title' => 'ShopIt Demo', 'primary_color' => '#111827', 'cart_editing_enabled' => true]); } if ($key === 'POST api/tenants/{tenant:codigo}/catalog-items') { return jsonBody([ 'type' => 'standard', 'category_id' => '{{category_id}}', 'slug' => 'producto-demo', 'nombre' => 'Producto Demo', 'descripcion' => 'Producto creado desde Postman', 'precio' => 10000, 'inventory_policy' => 'tracked', 'inventory_subject' => 'product', 'max_units_per_user' => 5, 'has_tickets' => false, 'real_stock' => 100, 'images' => [TINY_PNG], ]); } if ($key === 'POST api/storage-test/s3/upload') { return formDataBody(['path' => 'postman/test-file.png', 'expires_in_minutes' => '60'], ['file']); } if ($key === 'POST api/v1/adminapp/tenant/desfile/entries/image') { return formDataBody(['is_enabled' => '1'], ['image']); } return null; } /** @return array */ function queryFor(string $uri): array { return match ($uri) { 'api/tenants/bootstrap' => [['key' => 'dominio', 'value' => '{{tenant_domain}}'], ['key' => 'path', 'value' => '/']], 'api/v1/adminapp/bootstrap/{dominio}', 'api/v1/scanner/bootstrap/{dominio}' => [['key' => 'path', 'value' => '/']], 'api/tenants/{tenant:codigo}/catalog-items' => [['key' => 'q', 'value' => 'demo'], ['key' => 'page', 'value' => '1']], 'api/tenants/{tenant:codigo}/catalog-items/{catalogItem}' => [['key' => 'variant_id', 'value' => '{{variant_id}}', 'disabled' => true]], 'api/tenants/{tenant:codigo}/catalog/featured-groups/{featuredGroup}/items', 'api/tenants/{tenant:codigo}/categories/{category}' => [['key' => 'page', 'value' => '1']], 'api/tenants/{tenant:codigo}/compras' => [['key' => 'status', 'value' => 'pending_payment', 'disabled' => true], ['key' => 'page', 'value' => '1']], 'api/v1/adminapp/tenant/sales', 'api/v1/adminapp/tenant/sales/pdf' => [ ['key' => 'q', 'value' => '', 'disabled' => true], ['key' => 'status', 'value' => 'confirmed', 'disabled' => true], ['key' => 'sale_date', 'value' => '2026-12-01', 'disabled' => true], ['key' => 'sort_by', 'value' => 'date'], ['key' => 'sort_direction', 'value' => 'desc'], ['key' => 'page', 'value' => '1'], ['key' => 'per_page', 'value' => '20'], ], 'api/v1/scanner/tickets' => [['key' => 'q', 'value' => '', 'disabled' => true], ['key' => 'page', 'value' => '1'], ['key' => 'per_page', 'value' => '20']], 'api/storage-test/s3/temporary-url' => [['key' => 'path', 'value' => '{{s3_path}}'], ['key' => 'expires_in_minutes', 'value' => '60']], default => [], }; } /** @return array{0: string, 1: string} */ function folderFor(string $uri, string $action): array { preg_match('/App\\\\Domains\\\\([^\\\\]+)/', $action, $matches); $domain = $matches[1] ?? 'Other'; if (str_starts_with($uri, 'api/v1/adminapp/')) { return ['Admin App API', $domain]; } if (str_starts_with($uri, 'api/v1/scanner/')) { return ['Scanner API', $domain]; } if (str_starts_with($uri, 'api/webhooks/')) { return ['Webhooks', $domain]; } if (in_array($domain, ['StorageTest', 'MailTest'], true)) { return ['Developer Utilities', $domain]; } if (in_array($domain, ['Client', 'Integration', 'Menu', 'Tenant'], true) && ! str_contains($uri, '{tenant:codigo}')) { return ['Platform Management', $domain]; } return ['Storefront API', $domain]; } function humanize(string $value): string { $value = preg_replace('/Controller$/', '', $value); $value = preg_replace('/(? 'List', 'store' => 'Create', 'show' => 'Get', 'update' => 'Update', 'destroy' => 'Delete', 'addItem' => 'Add Item', 'updateItemQuantity' => 'Update Item Quantity', 'updateCheckoutItem' => 'Update Checkout Cart Item', 'prepareCheckoutEditing' => 'Prepare Checkout Cart Editing', 'removeItem' => 'Remove Item', 'search' => 'Search', 'category' => 'Get Category', 'featuredGroupItems' => 'List Featured Group Items', 'variantOptions' => 'Get Variant Options', 'startCheckout' => 'Start Checkout', 'updateCustomerData' => 'Update Customer Data', 'paymentIntent' => 'Create Payment Intent', 'submitForReview' => 'Submit for Review', 'cancel' => 'Cancel', 'complete' => 'Complete', 'downloadPdf' => 'Download PDF', 'downloadModificationsPdf' => 'Download Modifications PDF', 'modifications' => 'List Modifications', 'tickets' => 'List Tickets', 'confirm' => 'Confirm', 'temporaryUrl' => 'Generate Temporary URL', 'replaceImage' => 'Replace Image', 'updateImage' => 'Update Image', 'destroyImage' => 'Delete Image', 'showExtra' => 'Get Extra', 'toggle' => 'Toggle', 'scan' => 'Scan', 'handle' => 'Receive', ]; $name = $handler === '__invoke' ? $controller : (($verbs[$handler] ?? humanize($handler)).' '.$controller); return $multiMethod ? $name.' ('.$method.')' : $name; } function pathFor(string $uri): string { $variables = [ 'tenant:codigo' => 'tenant_code', 'tenant' => 'tenant_id', 'client' => 'client_id', 'integration_code' => 'integration_code', 'integration' => 'integration_id', 'menue' => 'menu_id', 'menu_code' => 'menu_code', 'tenant_code' => 'tenant_code', 'catalogItem' => 'catalog_item_id', 'featuredGroup' => 'featured_group_id', 'category' => 'category_id', 'cartItem' => 'cart_item_id', 'compra' => 'purchase_id', 'item' => 'purchase_item_id', 'dominio' => 'tenant_domain', 'sale' => 'sale_id', 'staff' => 'staff_id', 'websiteExtraCode' => 'website_extra_code', 'accommodation' => 'accommodation_id', 'entry' => 'entry_id', 'food' => 'food_id', 'merchandise' => 'merchandise_id', 'ticketUuid' => 'ticket_uuid', ]; return preg_replace_callback('/\{([^}]+)\}/', function (array $matches) use ($variables): string { $variable = $variables[$matches[1]] ?? strtolower((string) preg_replace('/(? */ function authFor(string $uri, array $middleware): ?array { $protected = count(array_filter( $middleware, fn (string $item): bool => str_contains($item, 'Authenticate:sanctum'), )) > 0; if (! $protected) { return null; } $variable = str_starts_with($uri, 'api/v1/adminapp/') ? 'admin_token' : (str_starts_with($uri, 'api/v1/scanner/') ? 'scanner_token' : 'token'); return ['type' => 'bearer', 'bearer' => [['key' => 'token', 'value' => '{{'.$variable.'}}', 'type' => 'string']]]; } /** @return array */ function tokenCaptureEvent(string $uri): array { $variable = $uri === 'api/v1/adminapp/login' ? 'admin_token' : ($uri === 'api/v1/scanner/login' ? 'scanner_token' : 'token'); return [ 'listen' => 'test', 'script' => [ 'type' => 'text/javascript', 'exec' => [ 'if (pm.response.code >= 200 && pm.response.code < 300) {', ' const payload = pm.response.json();', " if (payload.token) pm.collectionVariables.set('{$variable}', payload.token);", '}', ], ], ]; } $tree = []; $registeredOperations = 0; foreach ($routes as $route) { $methods = array_values(array_filter(explode('|', $route['method']), fn (string $method): bool => $method !== 'HEAD')); $multiMethod = count($methods) > 1; foreach ($methods as $method) { [$section, $folder] = folderFor($route['uri'], $route['action']); $path = pathFor($route['uri']); $query = queryFor($route['uri']); $enabledQuery = array_values(array_filter($query, fn (array $item): bool => ! ($item['disabled'] ?? false))); $queryString = $enabledQuery === [] ? '' : '?'.implode('&', array_map( fn (array $item): string => rawurlencode($item['key']).'='.$item['value'], $enabledQuery, )); $body = bodyFor($method, $route['uri']); $auth = authFor($route['uri'], $route['middleware']); $headers = [['key' => 'Accept', 'value' => 'application/json', 'type' => 'text']]; if (($body['mode'] ?? null) === 'raw') { $headers[] = ['key' => 'Content-Type', 'value' => 'application/json', 'type' => 'text']; } $request = [ 'method' => $method, 'header' => $headers, 'description' => sprintf( "Ruta Laravel: `%s /%s`\n\nControlador: `%s`%s", $method, $route['uri'], $route['action'], $auth === null ? '' : "\n\nRequiere autenticación Sanctum.", ), 'url' => [ 'raw' => '{{base_url}}/'.$path.$queryString, 'host' => ['{{base_url}}'], 'path' => explode('/', $path), ], ]; if ($query !== []) { $request['url']['query'] = $query; } if ($body !== null) { $request['body'] = $body; } if ($auth !== null) { $request['auth'] = $auth; } $item = [ 'name' => requestName($method, $route['action'], $multiMethod), 'request' => $request, 'response' => [], ]; if (in_array($route['uri'], ['api/login', 'api/v1/adminapp/login', 'api/v1/scanner/login'], true)) { $item['event'] = [tokenCaptureEvent($route['uri'])]; } $tree[$section][$folder][] = $item; $registeredOperations++; } } $sectionDescriptions = [ 'Storefront API' => 'API pública y autenticada consumida por el storefront: autenticación, catálogo, carrito, compras y tickets.', 'Admin App API' => 'Operaciones de administración del tenant. Ejecutá Login primero para completar `admin_token`.', 'Scanner API' => 'Operaciones de la aplicación de escaneo. Ejecutá Login primero para completar `scanner_token`.', 'Platform Management' => 'CRUD y configuración de tenants, clientes, menús e integraciones.', 'Webhooks' => 'Entradas públicas para notificaciones de proveedores externos.', 'Developer Utilities' => 'Endpoints de diagnóstico de correo y almacenamiento. No deberían exponerse en producción.', ]; $items = []; foreach ($tree as $section => $folders) { $children = []; foreach ($folders as $folder => $requests) { $children[] = ['name' => humanize($folder), 'item' => $requests]; } $items[] = [ 'name' => $section, 'description' => $sectionDescriptions[$section] ?? '', 'item' => $children, ]; } $variables = [ 'base_url' => 'http://localhost:8000', 'token' => '', 'admin_token' => '', 'scanner_token' => '', 'user_email' => 'usuario@example.com', 'user_password' => 'Password!123', 'admin_email' => 'admin@example.com', 'admin_password' => 'Password!123', 'scanner_email' => 'scanner@example.com', 'scanner_password' => 'Password!123', 'tenant_code' => 'demo', 'tenant_id' => '1', 'tenant_domain' => 'demo.test', 'client_id' => '1', 'integration_id' => '1', 'integration_code' => 'telepagos', 'menu_id' => '1', 'menu_code' => 'demo', 'catalog_item_id' => '1', 'variant_id' => '1', 'category_id' => '1', 'featured_group_id' => '1', 'cart_id' => '1', 'cart_item_id' => '1', 'purchase_id' => '1', 'purchase_item_id' => '1', 'sale_id' => '1', 'staff_id' => '1', 'website_extra_code' => 'hero', 'accommodation_id' => '1', 'entry_id' => '1', 'food_id' => '1', 'merchandise_id' => '1', 'ticket_uuid' => '00000000-0000-0000-0000-000000000000', 'oauth_code' => '00000000-0000-0000-0000-000000000000', 'reset_code' => '1234', 's3_path' => 'postman/test-file.png', ]; $collection = [ 'info' => [ '_postman_id' => '76fd6fd2-53b9-4d92-8e02-1ddcc6207fa2', 'name' => 'ShopIt API — Complete', 'description' => "Colección canónica generada desde las rutas reales de Laravel. Incluye {$registeredOperations} operaciones HTTP, ejemplos de payload, filtros, archivos y tokens separados para Storefront, Admin App y Scanner.\n\nUso rápido:\n1. Ajustá `base_url` y las credenciales.\n2. Ejecutá el Login de la aplicación correspondiente; el token se guarda automáticamente.\n3. Ajustá los IDs y códigos de las variables de colección.\n\nRegeneración: `php postman/generate-shopit-collection.php`.", 'schema' => 'https://schema.getpostman.com/json/collection/v2.1.0/collection.json', ], 'item' => $items, 'variable' => array_map( fn (string $key, string $value): array => ['key' => $key, 'value' => $value, 'type' => 'string'], array_keys($variables), array_values($variables), ), ]; $output = $root.DIRECTORY_SEPARATOR.'ShopIt_API_Postman_Collection.json'; file_put_contents($output, json_encode($collection, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE).PHP_EOL); fwrite(STDOUT, "Generated {$registeredOperations} operations in {$output}\n");