fase 1
This commit is contained in:
@@ -0,0 +1,117 @@
|
|||||||
|
name: Build and Push Medusa
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
paths:
|
||||||
|
- 'workloads/commerce-backend/**'
|
||||||
|
- '.gitea/workflows/build-medusa.yaml'
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
packages: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: Construir y publicar Medusa
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 30
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout del código
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
fetch-depth: 1
|
||||||
|
persist-credentials: true
|
||||||
|
|
||||||
|
- name: Definir versión
|
||||||
|
id: vars
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
echo "VERSION=v1.0.${{ github.run_number }}" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
- name: Validar secretos del Registry
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
REGISTRY_USER: ${{ secrets.REGISTRY_USER }}
|
||||||
|
REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
test -n "${REGISTRY_USER}" || {
|
||||||
|
echo "ERROR: REGISTRY_USER no está configurado."
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
test -n "${REGISTRY_PASSWORD}" || {
|
||||||
|
echo "ERROR: REGISTRY_PASSWORD no está configurado."
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
- name: Login en Gitea Registry
|
||||||
|
uses: docker/login-action@v2
|
||||||
|
with:
|
||||||
|
registry: gitea.cruzcloud.net
|
||||||
|
username: ${{ secrets.REGISTRY_USER }}
|
||||||
|
password: ${{ secrets.REGISTRY_PASSWORD }}
|
||||||
|
logout: true
|
||||||
|
|
||||||
|
- name: Construir y subir imagen
|
||||||
|
uses: docker/build-push-action@v4
|
||||||
|
with:
|
||||||
|
context: workloads/commerce-backend/
|
||||||
|
file: workloads/commerce-backend/Dockerfile
|
||||||
|
push: true
|
||||||
|
tags: |
|
||||||
|
gitea.cruzcloud.net/devops/ecommerce-medusa:${{ steps.vars.outputs.VERSION }}
|
||||||
|
gitea.cruzcloud.net/devops/ecommerce-medusa:latest
|
||||||
|
|
||||||
|
- name: Verificar promoción segura
|
||||||
|
id: promotion
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
git fetch origin main
|
||||||
|
|
||||||
|
CURRENT_SHA="${{ github.sha }}"
|
||||||
|
REMOTE_SHA="$(git rev-parse origin/main)"
|
||||||
|
|
||||||
|
if [ "${CURRENT_SHA}" = "${REMOTE_SHA}" ]; then
|
||||||
|
echo "promote=true" >> "$GITHUB_OUTPUT"
|
||||||
|
else
|
||||||
|
echo "promote=false" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "Hay un commit más reciente; no se actualizará el manifiesto."
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Actualizar manifiesto Medusa
|
||||||
|
if: steps.promotion.outputs.promote == 'true'
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
VERSION="${{ steps.vars.outputs.VERSION }}"
|
||||||
|
MANIFEST="workloads/ecommerce/commerce/medusa.yaml"
|
||||||
|
IMAGE="gitea.cruzcloud.net/devops/ecommerce-medusa"
|
||||||
|
|
||||||
|
git config user.name "gitea-actions"
|
||||||
|
git config user.email "[email protected]"
|
||||||
|
|
||||||
|
git fetch origin main
|
||||||
|
git checkout -B main origin/main
|
||||||
|
|
||||||
|
sed -i -E \
|
||||||
|
"s|(${IMAGE}:)v[0-9]+\.[0-9]+\.[0-9]+|\1${VERSION}|g" \
|
||||||
|
"${MANIFEST}"
|
||||||
|
|
||||||
|
git add "${MANIFEST}"
|
||||||
|
|
||||||
|
if git diff --cached --quiet; then
|
||||||
|
echo "El manifiesto ya apunta a ${VERSION}."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
git commit \
|
||||||
|
-m "chore(gitops): deploy Medusa ${VERSION} [skip ci]"
|
||||||
|
|
||||||
|
git push origin HEAD:main
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
node_modules
|
||||||
|
.medusa
|
||||||
|
.cache
|
||||||
|
.git
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
npm-debug.log*
|
||||||
|
README.md
|
||||||
|
data
|
||||||
|
migration
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
fund=false
|
||||||
|
audit=false
|
||||||
|
legacy-peer-deps=true
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
# syntax=docker/dockerfile:1.7
|
||||||
|
|
||||||
|
FROM node:22.18.0-bookworm-slim AS builder
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
ENV CI=true
|
||||||
|
ENV NODE_ENV=development
|
||||||
|
ENV NPM_CONFIG_UPDATE_NOTIFIER=false
|
||||||
|
|
||||||
|
COPY package.json .npmrc ./
|
||||||
|
RUN --mount=type=cache,target=/root/.npm,sharing=locked \
|
||||||
|
npm install --no-audit --no-fund
|
||||||
|
|
||||||
|
COPY tsconfig.json medusa-config.ts ./
|
||||||
|
COPY src ./src
|
||||||
|
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
FROM node:22.18.0-bookworm-slim AS runtime
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
ENV PORT=9000
|
||||||
|
ENV NPM_CONFIG_UPDATE_NOTIFIER=false
|
||||||
|
|
||||||
|
COPY --from=builder /app/.medusa/server/package.json ./
|
||||||
|
RUN --mount=type=cache,target=/root/.npm,sharing=locked \
|
||||||
|
npm install --omit=dev --no-audit --no-fund
|
||||||
|
|
||||||
|
COPY --from=builder /app/.medusa/server ./
|
||||||
|
|
||||||
|
RUN chown -R node:node /app
|
||||||
|
USER node
|
||||||
|
|
||||||
|
EXPOSE 9000
|
||||||
|
|
||||||
|
CMD ["npx", "medusa", "start"]
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
# ARI Shopping Commerce Backend
|
||||||
|
|
||||||
|
Backend Medusa v2.18 para la Fase 1.
|
||||||
|
|
||||||
|
## Capacidades iniciales
|
||||||
|
|
||||||
|
- Medusa Admin en `/app`.
|
||||||
|
- Catálogo, variantes y precios.
|
||||||
|
- Imágenes en almacenamiento S3 compatible.
|
||||||
|
- PostgreSQL y Redis externos.
|
||||||
|
- Modo `shared` para una sola réplica durante la Fase 1.
|
||||||
|
- Migraciones ejecutadas por el `initContainer` de Kubernetes.
|
||||||
|
|
||||||
|
La imagen se construye mediante:
|
||||||
|
|
||||||
|
```text
|
||||||
|
.gitea/workflows/build-medusa.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
No se deben colocar secretos en este directorio.
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
import { defineConfig, loadEnv } from "@medusajs/framework/utils"
|
||||||
|
|
||||||
|
loadEnv(process.env.NODE_ENV || "development", process.cwd())
|
||||||
|
|
||||||
|
const required = (name: string): string => {
|
||||||
|
const value = process.env[name]
|
||||||
|
if (!value) {
|
||||||
|
throw new Error(`La variable ${name} es obligatoria.`)
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = defineConfig({
|
||||||
|
projectConfig: {
|
||||||
|
databaseUrl: required("DATABASE_URL"),
|
||||||
|
redisUrl: required("REDIS_URL"),
|
||||||
|
workerMode: (process.env.MEDUSA_WORKER_MODE || "shared") as
|
||||||
|
| "shared"
|
||||||
|
| "worker"
|
||||||
|
| "server",
|
||||||
|
http: {
|
||||||
|
storeCors: required("STORE_CORS"),
|
||||||
|
adminCors: required("ADMIN_CORS"),
|
||||||
|
authCors: required("AUTH_CORS"),
|
||||||
|
jwtSecret: required("JWT_SECRET"),
|
||||||
|
cookieSecret: required("COOKIE_SECRET"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
admin: {
|
||||||
|
disable: process.env.DISABLE_MEDUSA_ADMIN === "true",
|
||||||
|
backendUrl:
|
||||||
|
process.env.MEDUSA_BACKEND_URL ||
|
||||||
|
"https://commerce.cruzcloud.net",
|
||||||
|
storefrontUrl:
|
||||||
|
process.env.STOREFRONT_URL ||
|
||||||
|
"https://shop.cruzcloud.net",
|
||||||
|
},
|
||||||
|
|
||||||
|
modules: [
|
||||||
|
{
|
||||||
|
resolve: "@medusajs/medusa/file",
|
||||||
|
options: {
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
resolve: "@medusajs/medusa/file-s3",
|
||||||
|
id: "s3",
|
||||||
|
options: {
|
||||||
|
file_url: required("S3_FILE_URL"),
|
||||||
|
access_key_id: required("S3_ACCESS_KEY_ID"),
|
||||||
|
secret_access_key: required("S3_SECRET_ACCESS_KEY"),
|
||||||
|
region: process.env.S3_REGION || "us-east-1",
|
||||||
|
bucket: required("S3_BUCKET"),
|
||||||
|
endpoint: required("S3_ENDPOINT"),
|
||||||
|
cache_control:
|
||||||
|
"public, max-age=31536000, immutable",
|
||||||
|
additional_client_config: {
|
||||||
|
forcePathStyle: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
{
|
||||||
|
"name": "ari-shopping-commerce-backend",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"private": true,
|
||||||
|
"description": "Backend de catálogo dinámico ARI Shopping sobre Medusa v2.",
|
||||||
|
"license": "UNLICENSED",
|
||||||
|
"scripts": {
|
||||||
|
"build": "medusa build",
|
||||||
|
"start": "medusa start",
|
||||||
|
"dev": "medusa develop",
|
||||||
|
"predeploy": "medusa db:migrate"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@medusajs/admin-sdk": "2.18.0",
|
||||||
|
"@medusajs/cli": "2.18.0",
|
||||||
|
"@medusajs/framework": "2.18.0",
|
||||||
|
"@medusajs/medusa": "2.18.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@swc/core": "^1.7.28",
|
||||||
|
"@types/node": "^20.12.11",
|
||||||
|
"@types/react": "^18.3.2",
|
||||||
|
"@types/react-dom": "^18.2.25",
|
||||||
|
"react": "^18.3.1",
|
||||||
|
"react-dom": "^18.3.1",
|
||||||
|
"typescript": "^5.6.2",
|
||||||
|
"vite": "^5.4.14"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2021",
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"module": "Node16",
|
||||||
|
"moduleResolution": "Node16",
|
||||||
|
"emitDecoratorMetadata": true,
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"skipDefaultLibCheck": true,
|
||||||
|
"declaration": false,
|
||||||
|
"sourceMap": false,
|
||||||
|
"inlineSourceMap": true,
|
||||||
|
"outDir": "./.medusa/server",
|
||||||
|
"rootDir": "./",
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"checkJs": false,
|
||||||
|
"strictNullChecks": true
|
||||||
|
},
|
||||||
|
"ts-node": {
|
||||||
|
"swc": true
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"**/*",
|
||||||
|
".medusa/types/*"
|
||||||
|
],
|
||||||
|
"exclude": [
|
||||||
|
"node_modules",
|
||||||
|
".medusa/server",
|
||||||
|
".medusa/admin",
|
||||||
|
".cache"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
# Cambios a `workloads/ecommerce/kustomization.yaml`
|
||||||
|
|
||||||
|
El instalador incluido agrega solamente el backend de la Fase 1:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
resources:
|
||||||
|
- commerce/
|
||||||
|
```
|
||||||
|
|
||||||
|
No activa automáticamente Medusa en el frontend.
|
||||||
|
|
||||||
|
Después de validar el backend, agrega este patch al bloque `patches`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- path: patches/frontend-medusa-env.yaml
|
||||||
|
target:
|
||||||
|
group: apps
|
||||||
|
version: v1
|
||||||
|
kind: Deployment
|
||||||
|
name: frontend-deploy
|
||||||
|
```
|
||||||
|
|
||||||
|
El catálogo `mock` seguirá funcionando hasta realizar este commit de corte.
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: commerce-config
|
||||||
|
data:
|
||||||
|
NODE_ENV: production
|
||||||
|
PORT: "9000"
|
||||||
|
|
||||||
|
STORE_CORS: https://shop.cruzcloud.net
|
||||||
|
ADMIN_CORS: https://commerce.cruzcloud.net
|
||||||
|
AUTH_CORS: https://commerce.cruzcloud.net,https://shop.cruzcloud.net
|
||||||
|
|
||||||
|
MEDUSA_BACKEND_URL: https://commerce.cruzcloud.net
|
||||||
|
STOREFRONT_URL: https://shop.cruzcloud.net
|
||||||
|
MEDUSA_WORKER_MODE: shared
|
||||||
|
DISABLE_MEDUSA_ADMIN: "false"
|
||||||
|
|
||||||
|
S3_FILE_URL: https://media.cruzcloud.net/ari-products
|
||||||
|
S3_ENDPOINT: http://minio-svc:9000
|
||||||
|
S3_REGION: us-east-1
|
||||||
|
S3_BUCKET: ari-products
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: Ingress
|
||||||
|
metadata:
|
||||||
|
name: commerce-ingress
|
||||||
|
annotations:
|
||||||
|
traefik.ingress.kubernetes.io/router.entrypoints: web
|
||||||
|
spec:
|
||||||
|
ingressClassName: traefik
|
||||||
|
rules:
|
||||||
|
- host: commerce.cruzcloud.net
|
||||||
|
http:
|
||||||
|
paths:
|
||||||
|
- path: /
|
||||||
|
pathType: Prefix
|
||||||
|
backend:
|
||||||
|
service:
|
||||||
|
name: medusa-svc
|
||||||
|
port:
|
||||||
|
number: 9000
|
||||||
|
|
||||||
|
- host: media.cruzcloud.net
|
||||||
|
http:
|
||||||
|
paths:
|
||||||
|
- path: /
|
||||||
|
pathType: Prefix
|
||||||
|
backend:
|
||||||
|
service:
|
||||||
|
name: minio-svc
|
||||||
|
port:
|
||||||
|
number: 9000
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||||
|
kind: Kustomization
|
||||||
|
|
||||||
|
resources:
|
||||||
|
- configmap.yaml
|
||||||
|
- postgres.yaml
|
||||||
|
- redis.yaml
|
||||||
|
- minio.yaml
|
||||||
|
- minio-bootstrap-job.yaml
|
||||||
|
- medusa.yaml
|
||||||
|
- ingress-commerce.yaml
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: medusa-deploy
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
strategy:
|
||||||
|
type: RollingUpdate
|
||||||
|
rollingUpdate:
|
||||||
|
maxUnavailable: 0
|
||||||
|
maxSurge: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: medusa
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: medusa
|
||||||
|
spec:
|
||||||
|
imagePullSecrets:
|
||||||
|
- name: gitea-registry-secret
|
||||||
|
|
||||||
|
initContainers:
|
||||||
|
- name: migrations
|
||||||
|
image: gitea.cruzcloud.net/devops/ecommerce-medusa:v1.0.0
|
||||||
|
imagePullPolicy: IfNotPresent
|
||||||
|
command:
|
||||||
|
- npx
|
||||||
|
- medusa
|
||||||
|
- db:migrate
|
||||||
|
envFrom:
|
||||||
|
- configMapRef:
|
||||||
|
name: commerce-config
|
||||||
|
- secretRef:
|
||||||
|
name: commerce-secrets
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: 50m
|
||||||
|
memory: 256Mi
|
||||||
|
limits:
|
||||||
|
cpu: 500m
|
||||||
|
memory: 768Mi
|
||||||
|
|
||||||
|
containers:
|
||||||
|
- name: medusa
|
||||||
|
image: gitea.cruzcloud.net/devops/ecommerce-medusa:v1.0.0
|
||||||
|
imagePullPolicy: IfNotPresent
|
||||||
|
envFrom:
|
||||||
|
- configMapRef:
|
||||||
|
name: commerce-config
|
||||||
|
- secretRef:
|
||||||
|
name: commerce-secrets
|
||||||
|
ports:
|
||||||
|
- name: http
|
||||||
|
containerPort: 9000
|
||||||
|
readinessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /health
|
||||||
|
port: http
|
||||||
|
initialDelaySeconds: 20
|
||||||
|
periodSeconds: 10
|
||||||
|
timeoutSeconds: 5
|
||||||
|
failureThreshold: 18
|
||||||
|
livenessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /health
|
||||||
|
port: http
|
||||||
|
initialDelaySeconds: 60
|
||||||
|
periodSeconds: 20
|
||||||
|
timeoutSeconds: 5
|
||||||
|
failureThreshold: 6
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: 150m
|
||||||
|
memory: 384Mi
|
||||||
|
limits:
|
||||||
|
cpu: 750m
|
||||||
|
memory: 1Gi
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: medusa-svc
|
||||||
|
spec:
|
||||||
|
selector:
|
||||||
|
app: medusa
|
||||||
|
ports:
|
||||||
|
- name: http
|
||||||
|
port: 9000
|
||||||
|
targetPort: 9000
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
apiVersion: batch/v1
|
||||||
|
kind: Job
|
||||||
|
metadata:
|
||||||
|
name: commerce-minio-bootstrap
|
||||||
|
spec:
|
||||||
|
backoffLimit: 12
|
||||||
|
ttlSecondsAfterFinished: 86400
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: commerce-minio-bootstrap
|
||||||
|
spec:
|
||||||
|
restartPolicy: OnFailure
|
||||||
|
containers:
|
||||||
|
- name: configure
|
||||||
|
image: quay.io/minio/mc:RELEASE.2025-08-13T08-35-41Z
|
||||||
|
imagePullPolicy: IfNotPresent
|
||||||
|
env:
|
||||||
|
- name: MINIO_ROOT_USER
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: commerce-secrets
|
||||||
|
key: MINIO_ROOT_USER
|
||||||
|
- name: MINIO_ROOT_PASSWORD
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: commerce-secrets
|
||||||
|
key: MINIO_ROOT_PASSWORD
|
||||||
|
command:
|
||||||
|
- sh
|
||||||
|
- -c
|
||||||
|
args:
|
||||||
|
- |
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
until mc alias set local \
|
||||||
|
http://minio-svc:9000 \
|
||||||
|
"$MINIO_ROOT_USER" \
|
||||||
|
"$MINIO_ROOT_PASSWORD"; do
|
||||||
|
echo "Esperando MinIO..."
|
||||||
|
sleep 5
|
||||||
|
done
|
||||||
|
|
||||||
|
mc mb --ignore-existing local/ari-products
|
||||||
|
mc anonymous set download local/ari-products
|
||||||
|
|
||||||
|
echo "Bucket ari-products creado y habilitado para lectura pública."
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: 10m
|
||||||
|
memory: 32Mi
|
||||||
|
limits:
|
||||||
|
cpu: 100m
|
||||||
|
memory: 128Mi
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: minio-svc
|
||||||
|
spec:
|
||||||
|
selector:
|
||||||
|
app: commerce-minio
|
||||||
|
ports:
|
||||||
|
- name: s3
|
||||||
|
port: 9000
|
||||||
|
targetPort: 9000
|
||||||
|
- name: console
|
||||||
|
port: 9001
|
||||||
|
targetPort: 9001
|
||||||
|
---
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: StatefulSet
|
||||||
|
metadata:
|
||||||
|
name: commerce-minio
|
||||||
|
spec:
|
||||||
|
serviceName: minio-svc
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: commerce-minio
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: commerce-minio
|
||||||
|
spec:
|
||||||
|
terminationGracePeriodSeconds: 60
|
||||||
|
containers:
|
||||||
|
- name: minio
|
||||||
|
image: quay.io/minio/minio:RELEASE.2025-10-15T17-29-55Z
|
||||||
|
imagePullPolicy: IfNotPresent
|
||||||
|
args:
|
||||||
|
- server
|
||||||
|
- /data
|
||||||
|
- --console-address
|
||||||
|
- ":9001"
|
||||||
|
env:
|
||||||
|
- name: MINIO_ROOT_USER
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: commerce-secrets
|
||||||
|
key: MINIO_ROOT_USER
|
||||||
|
- name: MINIO_ROOT_PASSWORD
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: commerce-secrets
|
||||||
|
key: MINIO_ROOT_PASSWORD
|
||||||
|
ports:
|
||||||
|
- name: s3
|
||||||
|
containerPort: 9000
|
||||||
|
- name: console
|
||||||
|
containerPort: 9001
|
||||||
|
readinessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /minio/health/ready
|
||||||
|
port: s3
|
||||||
|
initialDelaySeconds: 10
|
||||||
|
periodSeconds: 10
|
||||||
|
timeoutSeconds: 5
|
||||||
|
failureThreshold: 12
|
||||||
|
livenessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /minio/health/live
|
||||||
|
port: s3
|
||||||
|
initialDelaySeconds: 30
|
||||||
|
periodSeconds: 20
|
||||||
|
timeoutSeconds: 5
|
||||||
|
failureThreshold: 6
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: 100m
|
||||||
|
memory: 256Mi
|
||||||
|
limits:
|
||||||
|
cpu: 500m
|
||||||
|
memory: 768Mi
|
||||||
|
volumeMounts:
|
||||||
|
- name: minio-data
|
||||||
|
mountPath: /data
|
||||||
|
volumeClaimTemplates:
|
||||||
|
- metadata:
|
||||||
|
name: minio-data
|
||||||
|
spec:
|
||||||
|
accessModes:
|
||||||
|
- ReadWriteOnce
|
||||||
|
storageClassName: local-path
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
storage: 20Gi
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: postgres-svc
|
||||||
|
spec:
|
||||||
|
clusterIP: None
|
||||||
|
selector:
|
||||||
|
app: commerce-postgres
|
||||||
|
ports:
|
||||||
|
- name: postgres
|
||||||
|
port: 5432
|
||||||
|
targetPort: 5432
|
||||||
|
---
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: StatefulSet
|
||||||
|
metadata:
|
||||||
|
name: commerce-postgres
|
||||||
|
spec:
|
||||||
|
serviceName: postgres-svc
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: commerce-postgres
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: commerce-postgres
|
||||||
|
spec:
|
||||||
|
terminationGracePeriodSeconds: 60
|
||||||
|
containers:
|
||||||
|
- name: postgres
|
||||||
|
image: postgres:17.10-alpine3.24
|
||||||
|
imagePullPolicy: IfNotPresent
|
||||||
|
ports:
|
||||||
|
- name: postgres
|
||||||
|
containerPort: 5432
|
||||||
|
env:
|
||||||
|
- name: POSTGRES_USER
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: commerce-secrets
|
||||||
|
key: POSTGRES_USER
|
||||||
|
- name: POSTGRES_PASSWORD
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: commerce-secrets
|
||||||
|
key: POSTGRES_PASSWORD
|
||||||
|
- name: POSTGRES_DB
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: commerce-secrets
|
||||||
|
key: POSTGRES_DB
|
||||||
|
- name: PGDATA
|
||||||
|
value: /var/lib/postgresql/data/pgdata
|
||||||
|
readinessProbe:
|
||||||
|
exec:
|
||||||
|
command:
|
||||||
|
- sh
|
||||||
|
- -c
|
||||||
|
- pg_isready -U "$POSTGRES_USER" -d "$POSTGRES_DB"
|
||||||
|
initialDelaySeconds: 10
|
||||||
|
periodSeconds: 10
|
||||||
|
timeoutSeconds: 5
|
||||||
|
failureThreshold: 6
|
||||||
|
livenessProbe:
|
||||||
|
exec:
|
||||||
|
command:
|
||||||
|
- sh
|
||||||
|
- -c
|
||||||
|
- pg_isready -U "$POSTGRES_USER" -d "$POSTGRES_DB"
|
||||||
|
initialDelaySeconds: 30
|
||||||
|
periodSeconds: 20
|
||||||
|
timeoutSeconds: 5
|
||||||
|
failureThreshold: 6
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: 100m
|
||||||
|
memory: 256Mi
|
||||||
|
limits:
|
||||||
|
cpu: 500m
|
||||||
|
memory: 768Mi
|
||||||
|
volumeMounts:
|
||||||
|
- name: postgres-data
|
||||||
|
mountPath: /var/lib/postgresql/data
|
||||||
|
volumeClaimTemplates:
|
||||||
|
- metadata:
|
||||||
|
name: postgres-data
|
||||||
|
spec:
|
||||||
|
accessModes:
|
||||||
|
- ReadWriteOnce
|
||||||
|
storageClassName: local-path
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
storage: 10Gi
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: redis-svc
|
||||||
|
spec:
|
||||||
|
clusterIP: None
|
||||||
|
selector:
|
||||||
|
app: commerce-redis
|
||||||
|
ports:
|
||||||
|
- name: redis
|
||||||
|
port: 6379
|
||||||
|
targetPort: 6379
|
||||||
|
---
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: StatefulSet
|
||||||
|
metadata:
|
||||||
|
name: commerce-redis
|
||||||
|
spec:
|
||||||
|
serviceName: redis-svc
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: commerce-redis
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: commerce-redis
|
||||||
|
spec:
|
||||||
|
terminationGracePeriodSeconds: 30
|
||||||
|
containers:
|
||||||
|
- name: redis
|
||||||
|
image: redis:7.4-alpine
|
||||||
|
imagePullPolicy: IfNotPresent
|
||||||
|
command:
|
||||||
|
- sh
|
||||||
|
- -c
|
||||||
|
args:
|
||||||
|
- >
|
||||||
|
exec redis-server
|
||||||
|
--appendonly yes
|
||||||
|
--appendfsync everysec
|
||||||
|
--requirepass "$REDIS_PASSWORD"
|
||||||
|
env:
|
||||||
|
- name: REDIS_PASSWORD
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: commerce-secrets
|
||||||
|
key: REDIS_PASSWORD
|
||||||
|
ports:
|
||||||
|
- name: redis
|
||||||
|
containerPort: 6379
|
||||||
|
readinessProbe:
|
||||||
|
exec:
|
||||||
|
command:
|
||||||
|
- sh
|
||||||
|
- -c
|
||||||
|
- redis-cli -a "$REDIS_PASSWORD" ping | grep PONG
|
||||||
|
initialDelaySeconds: 5
|
||||||
|
periodSeconds: 10
|
||||||
|
timeoutSeconds: 5
|
||||||
|
failureThreshold: 6
|
||||||
|
livenessProbe:
|
||||||
|
exec:
|
||||||
|
command:
|
||||||
|
- sh
|
||||||
|
- -c
|
||||||
|
- redis-cli -a "$REDIS_PASSWORD" ping | grep PONG
|
||||||
|
initialDelaySeconds: 20
|
||||||
|
periodSeconds: 20
|
||||||
|
timeoutSeconds: 5
|
||||||
|
failureThreshold: 6
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: 50m
|
||||||
|
memory: 64Mi
|
||||||
|
limits:
|
||||||
|
cpu: 250m
|
||||||
|
memory: 256Mi
|
||||||
|
volumeMounts:
|
||||||
|
- name: redis-data
|
||||||
|
mountPath: /data
|
||||||
|
volumeClaimTemplates:
|
||||||
|
- metadata:
|
||||||
|
name: redis-data
|
||||||
|
spec:
|
||||||
|
accessModes:
|
||||||
|
- ReadWriteOnce
|
||||||
|
storageClassName: local-path
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
storage: 1Gi
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
# NO agregar este archivo al kustomization ni subir valores reales a Gitea.
|
||||||
|
# Utiliza scripts/create-commerce-secrets.sh o reemplázalo por Sealed Secrets.
|
||||||
|
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Secret
|
||||||
|
metadata:
|
||||||
|
name: commerce-secrets
|
||||||
|
type: Opaque
|
||||||
|
stringData:
|
||||||
|
POSTGRES_USER: medusa
|
||||||
|
POSTGRES_PASSWORD: CAMBIAR
|
||||||
|
POSTGRES_DB: medusa
|
||||||
|
DATABASE_URL: postgresql://medusa:CAMBIAR@postgres-svc:5432/medusa
|
||||||
|
|
||||||
|
REDIS_PASSWORD: CAMBIAR
|
||||||
|
REDIS_URL: redis://:CAMBIAR@redis-svc:6379
|
||||||
|
|
||||||
|
MINIO_ROOT_USER: ari-minio
|
||||||
|
MINIO_ROOT_PASSWORD: CAMBIAR
|
||||||
|
S3_ACCESS_KEY_ID: ari-minio
|
||||||
|
S3_SECRET_ACCESS_KEY: CAMBIAR
|
||||||
|
|
||||||
|
JWT_SECRET: CAMBIAR
|
||||||
|
COOKIE_SECRET: CAMBIAR
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Secret
|
||||||
|
metadata:
|
||||||
|
name: commerce-storefront
|
||||||
|
type: Opaque
|
||||||
|
stringData:
|
||||||
|
MEDUSA_PUBLISHABLE_KEY: pk_CAMBIAR_DESPUES_DE_CREARLA_EN_MEDUSA
|
||||||
@@ -1,31 +1,563 @@
|
|||||||
import type { CatalogProvider, Product } from "@/lib/headless/types";
|
import type {
|
||||||
|
CatalogProvider,
|
||||||
|
Product,
|
||||||
|
ProductFilters,
|
||||||
|
ProductVariant,
|
||||||
|
} from "@/lib/headless/types";
|
||||||
|
|
||||||
async function medusaFetch<T>(path: string): Promise<T> {
|
type MedusaImage = {
|
||||||
const baseUrl = process.env.MEDUSA_BACKEND_URL;
|
url?: string | null;
|
||||||
if (!baseUrl) throw new Error("Medusa no está configurado.");
|
};
|
||||||
const response = await fetch(`${baseUrl.replace(/\/$/, "")}${path}`, {
|
|
||||||
headers: process.env.MEDUSA_PUBLISHABLE_KEY
|
type MedusaTag = {
|
||||||
? { "x-publishable-api-key": process.env.MEDUSA_PUBLISHABLE_KEY }
|
value?: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
type MedusaCategory = {
|
||||||
|
name?: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
type MedusaCollection = {
|
||||||
|
title?: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
type MedusaCalculatedPrice = {
|
||||||
|
calculated_amount?: number | null;
|
||||||
|
original_amount?: number | null;
|
||||||
|
currency_code?: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
type MedusaVariant = {
|
||||||
|
id: string;
|
||||||
|
title?: string | null;
|
||||||
|
sku?: string | null;
|
||||||
|
manage_inventory?: boolean | null;
|
||||||
|
allow_backorder?: boolean | null;
|
||||||
|
inventory_quantity?: number | null;
|
||||||
|
calculated_price?: MedusaCalculatedPrice | null;
|
||||||
|
options?: Array<{
|
||||||
|
value?: string | null;
|
||||||
|
option?: {
|
||||||
|
title?: string | null;
|
||||||
|
} | null;
|
||||||
|
}> | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
type MedusaProduct = {
|
||||||
|
id: string;
|
||||||
|
handle?: string | null;
|
||||||
|
title?: string | null;
|
||||||
|
subtitle?: string | null;
|
||||||
|
description?: string | null;
|
||||||
|
thumbnail?: string | null;
|
||||||
|
external_id?: string | null;
|
||||||
|
metadata?: Record<string, unknown> | null;
|
||||||
|
images?: MedusaImage[] | null;
|
||||||
|
tags?: MedusaTag[] | null;
|
||||||
|
categories?: MedusaCategory[] | null;
|
||||||
|
collection?: MedusaCollection | null;
|
||||||
|
variants?: MedusaVariant[] | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
type MedusaProductsResponse = {
|
||||||
|
products: MedusaProduct[];
|
||||||
|
count?: number;
|
||||||
|
limit?: number;
|
||||||
|
offset?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
type LegacyMeta = {
|
||||||
|
category: string;
|
||||||
|
collection: string;
|
||||||
|
brand: string;
|
||||||
|
ageRange: string;
|
||||||
|
featured: boolean;
|
||||||
|
isNew: boolean;
|
||||||
|
fallbackImage: string;
|
||||||
|
fallbackThumbnail: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const legacyProductMeta: Record<string, LegacyMeta> = {
|
||||||
|
"toy-story-5-rex": {
|
||||||
|
"category": "Figuras",
|
||||||
|
"collection": "Toy Story",
|
||||||
|
"brand": "Disney Pixar",
|
||||||
|
"ageRange": "3+",
|
||||||
|
"featured": true,
|
||||||
|
"isNew": true,
|
||||||
|
"fallbackImage": "https://shop.cruzcloud.net/images/products/ari-001-toy-story-5-rex.webp",
|
||||||
|
"fallbackThumbnail": "https://shop.cruzcloud.net/images/thumbnails/ari-001-toy-story-5-rex.webp"
|
||||||
|
},
|
||||||
|
"toy-story-5-buzz-lightyear": {
|
||||||
|
"category": "Figuras",
|
||||||
|
"collection": "Toy Story",
|
||||||
|
"brand": "Disney Pixar",
|
||||||
|
"ageRange": "3+",
|
||||||
|
"featured": true,
|
||||||
|
"isNew": true,
|
||||||
|
"fallbackImage": "https://shop.cruzcloud.net/images/products/ari-002-toy-story-5-buzz-lightyear.webp",
|
||||||
|
"fallbackThumbnail": "https://shop.cruzcloud.net/images/thumbnails/ari-002-toy-story-5-buzz-lightyear.webp"
|
||||||
|
},
|
||||||
|
"toy-story-5-jessie": {
|
||||||
|
"category": "Figuras",
|
||||||
|
"collection": "Toy Story",
|
||||||
|
"brand": "Disney Pixar",
|
||||||
|
"ageRange": "3+",
|
||||||
|
"featured": true,
|
||||||
|
"isNew": true,
|
||||||
|
"fallbackImage": "https://shop.cruzcloud.net/images/products/ari-003-toy-story-5-jessie.webp",
|
||||||
|
"fallbackThumbnail": "https://shop.cruzcloud.net/images/thumbnails/ari-003-toy-story-5-jessie.webp"
|
||||||
|
},
|
||||||
|
"toy-story-5-woody": {
|
||||||
|
"category": "Figuras",
|
||||||
|
"collection": "Toy Story",
|
||||||
|
"brand": "Disney Pixar",
|
||||||
|
"ageRange": "3+",
|
||||||
|
"featured": true,
|
||||||
|
"isNew": true,
|
||||||
|
"fallbackImage": "https://shop.cruzcloud.net/images/products/ari-004-toy-story-5-woody.webp",
|
||||||
|
"fallbackThumbnail": "https://shop.cruzcloud.net/images/thumbnails/ari-004-toy-story-5-woody.webp"
|
||||||
|
},
|
||||||
|
"intensamente-furia": {
|
||||||
|
"category": "Peluches",
|
||||||
|
"collection": "Intensamente",
|
||||||
|
"brand": "Disney",
|
||||||
|
"ageRange": "3+",
|
||||||
|
"featured": true,
|
||||||
|
"isNew": true,
|
||||||
|
"fallbackImage": "https://shop.cruzcloud.net/images/products/ari-005-intensamente-furia.webp",
|
||||||
|
"fallbackThumbnail": "https://shop.cruzcloud.net/images/thumbnails/ari-005-intensamente-furia.webp"
|
||||||
|
},
|
||||||
|
"intensamente-tristeza": {
|
||||||
|
"category": "Peluches",
|
||||||
|
"collection": "Intensamente",
|
||||||
|
"brand": "Disney",
|
||||||
|
"ageRange": "3+",
|
||||||
|
"featured": true,
|
||||||
|
"isNew": true,
|
||||||
|
"fallbackImage": "https://shop.cruzcloud.net/images/products/ari-006-intensamente-tristeza.webp",
|
||||||
|
"fallbackThumbnail": "https://shop.cruzcloud.net/images/thumbnails/ari-006-intensamente-tristeza.webp"
|
||||||
|
},
|
||||||
|
"minnie-y-mickey-mouse": {
|
||||||
|
"category": "Peluches",
|
||||||
|
"collection": "Mickey & Minnie",
|
||||||
|
"brand": "Disney",
|
||||||
|
"ageRange": "3+",
|
||||||
|
"featured": true,
|
||||||
|
"isNew": false,
|
||||||
|
"fallbackImage": "https://shop.cruzcloud.net/images/products/ari-007-minnie-y-mickey-mouse.webp",
|
||||||
|
"fallbackThumbnail": "https://shop.cruzcloud.net/images/thumbnails/ari-007-minnie-y-mickey-mouse.webp"
|
||||||
|
},
|
||||||
|
"angel-lilo-y-stitch": {
|
||||||
|
"category": "Peluches",
|
||||||
|
"collection": "Lilo & Stitch",
|
||||||
|
"brand": "Disney",
|
||||||
|
"ageRange": "3+",
|
||||||
|
"featured": true,
|
||||||
|
"isNew": false,
|
||||||
|
"fallbackImage": "https://shop.cruzcloud.net/images/products/ari-008-angel-lilo-y-stitch.webp",
|
||||||
|
"fallbackThumbnail": "https://shop.cruzcloud.net/images/thumbnails/ari-008-angel-lilo-y-stitch.webp"
|
||||||
|
},
|
||||||
|
"mochilas-disney-coleccion-marvel": {
|
||||||
|
"category": "Mochilas",
|
||||||
|
"collection": "Marvel",
|
||||||
|
"brand": "Marvel",
|
||||||
|
"ageRange": "6+",
|
||||||
|
"featured": false,
|
||||||
|
"isNew": false,
|
||||||
|
"fallbackImage": "https://shop.cruzcloud.net/images/products/ari-009-mochilas-disney-coleccion-marvel.webp",
|
||||||
|
"fallbackThumbnail": "https://shop.cruzcloud.net/images/thumbnails/ari-009-mochilas-disney-coleccion-marvel.webp"
|
||||||
|
},
|
||||||
|
"mochila-ligera-y-resistente-al-agua-de-disney-frozen-mochila-escolar-y-de-viaje-con-multiples-compa": {
|
||||||
|
"category": "Mochilas",
|
||||||
|
"collection": "Frozen",
|
||||||
|
"brand": "Disney",
|
||||||
|
"ageRange": "6+",
|
||||||
|
"featured": false,
|
||||||
|
"isNew": false,
|
||||||
|
"fallbackImage": "https://shop.cruzcloud.net/images/products/ari-010-mochila-ligera-y-resistente-al-agua-de-d.webp",
|
||||||
|
"fallbackThumbnail": "https://shop.cruzcloud.net/images/thumbnails/ari-010-mochila-ligera-y-resistente-al-agua-de-d.webp"
|
||||||
|
},
|
||||||
|
"mochila-escolar-de-spider-man-de-disney-moda-estudiantil-libro-de-gran-capacidad-mochila-ligera": {
|
||||||
|
"category": "Mochilas",
|
||||||
|
"collection": "Marvel",
|
||||||
|
"brand": "Marvel",
|
||||||
|
"ageRange": "6+",
|
||||||
|
"featured": false,
|
||||||
|
"isNew": false,
|
||||||
|
"fallbackImage": "https://shop.cruzcloud.net/images/products/ari-011-mochila-escolar-de-spider-man-de-disney-.webp",
|
||||||
|
"fallbackThumbnail": "https://shop.cruzcloud.net/images/thumbnails/ari-011-mochila-escolar-de-spider-man-de-disney-.webp"
|
||||||
|
},
|
||||||
|
"disney-mochila-de-poliester-ultraligera-y-transpirable-de-gran-tamano-y-multiples-colores-dimension": {
|
||||||
|
"category": "Mochilas",
|
||||||
|
"collection": "Disney",
|
||||||
|
"brand": "Disney",
|
||||||
|
"ageRange": "6+",
|
||||||
|
"featured": false,
|
||||||
|
"isNew": false,
|
||||||
|
"fallbackImage": "https://shop.cruzcloud.net/images/products/ari-012-disney-mochila-de-poliester-ultraligera-.webp",
|
||||||
|
"fallbackThumbnail": "https://shop.cruzcloud.net/images/thumbnails/ari-012-disney-mochila-de-poliester-ultraligera-.webp"
|
||||||
|
},
|
||||||
|
"mochila-pequena-bob-esponja": {
|
||||||
|
"category": "Mochilas",
|
||||||
|
"collection": "Bob Esponja",
|
||||||
|
"brand": "Disney",
|
||||||
|
"ageRange": "6+",
|
||||||
|
"featured": false,
|
||||||
|
"isNew": false,
|
||||||
|
"fallbackImage": "https://shop.cruzcloud.net/images/products/ari-013-mochila-pequena-bob-esponja.webp",
|
||||||
|
"fallbackThumbnail": "https://shop.cruzcloud.net/images/thumbnails/ari-013-mochila-pequena-bob-esponja.webp"
|
||||||
|
},
|
||||||
|
"mochila-casual-de-gran-capacidad-con-estampado-de-bob-esponja-unisex-bolsa-escolar-de-dibujos-anim": {
|
||||||
|
"category": "Mochilas",
|
||||||
|
"collection": "Bob Esponja",
|
||||||
|
"brand": "Disney",
|
||||||
|
"ageRange": "6+",
|
||||||
|
"featured": false,
|
||||||
|
"isNew": false,
|
||||||
|
"fallbackImage": "https://shop.cruzcloud.net/images/products/ari-014-mochila-casual-de-gran-capacidad-con-est.webp",
|
||||||
|
"fallbackThumbnail": "https://shop.cruzcloud.net/images/thumbnails/ari-014-mochila-casual-de-gran-capacidad-con-est.webp"
|
||||||
|
},
|
||||||
|
"mochila-escolar-estilo-britanico-de-disney-con-proteccion-de-la-columna-vertebral-nueva-mochila-de": {
|
||||||
|
"category": "Mochilas",
|
||||||
|
"collection": "Disney",
|
||||||
|
"brand": "Disney",
|
||||||
|
"ageRange": "6+",
|
||||||
|
"featured": false,
|
||||||
|
"isNew": false,
|
||||||
|
"fallbackImage": "https://shop.cruzcloud.net/images/products/ari-015-mochila-escolar-estilo-britanico-de-disn.webp",
|
||||||
|
"fallbackThumbnail": "https://shop.cruzcloud.net/images/thumbnails/ari-015-mochila-escolar-estilo-britanico-de-disn.webp"
|
||||||
|
},
|
||||||
|
"disney-mochila-iron-man-de-poliester-ultraligera-y-transpirable-de-gran-tamano-dimensiones-46-cm-x": {
|
||||||
|
"category": "Mochilas",
|
||||||
|
"collection": "Marvel",
|
||||||
|
"brand": "Marvel",
|
||||||
|
"ageRange": "6+",
|
||||||
|
"featured": false,
|
||||||
|
"isNew": false,
|
||||||
|
"fallbackImage": "https://shop.cruzcloud.net/images/products/ari-016-disney-mochila-iron-man-de-poliester-ult.webp",
|
||||||
|
"fallbackThumbnail": "https://shop.cruzcloud.net/images/thumbnails/ari-016-disney-mochila-iron-man-de-poliester-ult.webp"
|
||||||
|
},
|
||||||
|
"mochila-de-disney-marvel-capitan-america-de-gran-capacidad-ligera-y-resistente-a-salpicaduras-par": {
|
||||||
|
"category": "Mochilas",
|
||||||
|
"collection": "Marvel",
|
||||||
|
"brand": "Marvel",
|
||||||
|
"ageRange": "6+",
|
||||||
|
"featured": false,
|
||||||
|
"isNew": false,
|
||||||
|
"fallbackImage": "https://shop.cruzcloud.net/images/products/ari-017-mochila-de-disney-marvel-capitan-america.webp",
|
||||||
|
"fallbackThumbnail": "https://shop.cruzcloud.net/images/thumbnails/ari-017-mochila-de-disney-marvel-capitan-america.webp"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function backendUrl(): string {
|
||||||
|
const value = process.env.MEDUSA_BACKEND_URL;
|
||||||
|
if (!value) {
|
||||||
|
throw new Error("MEDUSA_BACKEND_URL no está configurado.");
|
||||||
|
}
|
||||||
|
return value.replace(/\/$/, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function medusaFetch<T>(
|
||||||
|
path: string,
|
||||||
|
searchParams?: URLSearchParams,
|
||||||
|
): Promise<T> {
|
||||||
|
const url = new URL(`${backendUrl()}${path}`);
|
||||||
|
|
||||||
|
if (searchParams) {
|
||||||
|
searchParams.forEach((value, key) => {
|
||||||
|
url.searchParams.append(key, value);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const publishableKey = process.env.MEDUSA_PUBLISHABLE_KEY;
|
||||||
|
|
||||||
|
const response = await fetch(url, {
|
||||||
|
headers: publishableKey
|
||||||
|
? { "x-publishable-api-key": publishableKey }
|
||||||
: undefined,
|
: undefined,
|
||||||
next: { revalidate: 300 },
|
next: {
|
||||||
|
revalidate: 60,
|
||||||
|
tags: ["medusa-products"],
|
||||||
|
},
|
||||||
});
|
});
|
||||||
if (!response.ok) throw new Error(`Medusa respondió HTTP ${response.status}`);
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const body = await response.text().catch(() => "");
|
||||||
|
throw new Error(
|
||||||
|
`Medusa respondió HTTP ${response.status}: ${body.slice(0, 300)}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (await response.json()) as T;
|
return (await response.json()) as T;
|
||||||
}
|
}
|
||||||
|
|
||||||
function notImplemented(): never {
|
function stringMetadata(
|
||||||
throw new Error("Completa el mapeo Medusa → Product en lib/headless/providers/medusa.ts.");
|
metadata: Record<string, unknown> | null | undefined,
|
||||||
|
key: string,
|
||||||
|
): string {
|
||||||
|
const value = metadata?.[key];
|
||||||
|
return typeof value === "string" ? value : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function booleanMetadata(
|
||||||
|
metadata: Record<string, unknown> | null | undefined,
|
||||||
|
key: string,
|
||||||
|
): boolean {
|
||||||
|
const value = metadata?.[key];
|
||||||
|
return value === true || value === "true";
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeCurrency(value?: string | null): string {
|
||||||
|
return (value || "COP").toUpperCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapVariant(variant: MedusaVariant): ProductVariant {
|
||||||
|
const option = variant.options?.[0];
|
||||||
|
|
||||||
|
const available =
|
||||||
|
variant.allow_backorder === true ||
|
||||||
|
variant.manage_inventory === false ||
|
||||||
|
(variant.inventory_quantity ?? 0) > 0;
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: variant.id,
|
||||||
|
name: option?.option?.title || "Presentación",
|
||||||
|
value: option?.value || variant.title || "Estándar",
|
||||||
|
available,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapProduct(product: MedusaProduct): Product {
|
||||||
|
const slug = product.handle || product.id;
|
||||||
|
const legacy = legacyProductMeta[slug];
|
||||||
|
|
||||||
|
const variant = product.variants?.[0];
|
||||||
|
const calculated = variant?.calculated_price;
|
||||||
|
|
||||||
|
const category =
|
||||||
|
product.categories?.[0]?.name ||
|
||||||
|
stringMetadata(product.metadata, "category") ||
|
||||||
|
legacy?.category ||
|
||||||
|
"Variedades";
|
||||||
|
|
||||||
|
const collection =
|
||||||
|
product.collection?.title ||
|
||||||
|
stringMetadata(product.metadata, "collection") ||
|
||||||
|
legacy?.collection ||
|
||||||
|
"";
|
||||||
|
|
||||||
|
const brand =
|
||||||
|
stringMetadata(product.metadata, "brand") ||
|
||||||
|
legacy?.brand ||
|
||||||
|
"ARI Shopping";
|
||||||
|
|
||||||
|
const ageRange =
|
||||||
|
stringMetadata(product.metadata, "ageRange") ||
|
||||||
|
legacy?.ageRange ||
|
||||||
|
"Todas";
|
||||||
|
|
||||||
|
const images = [
|
||||||
|
...(product.images || [])
|
||||||
|
.map((image) => image.url || "")
|
||||||
|
.filter(Boolean),
|
||||||
|
];
|
||||||
|
|
||||||
|
if (images.length === 0 && legacy?.fallbackImage) {
|
||||||
|
images.push(legacy.fallbackImage);
|
||||||
|
}
|
||||||
|
|
||||||
|
const thumbnail =
|
||||||
|
product.thumbnail ||
|
||||||
|
images[0] ||
|
||||||
|
legacy?.fallbackThumbnail ||
|
||||||
|
"/images/brand/logo-ari.png";
|
||||||
|
|
||||||
|
const price =
|
||||||
|
typeof calculated?.calculated_amount === "number"
|
||||||
|
? calculated.calculated_amount
|
||||||
|
: null;
|
||||||
|
|
||||||
|
const original =
|
||||||
|
typeof calculated?.original_amount === "number"
|
||||||
|
? calculated.original_amount
|
||||||
|
: null;
|
||||||
|
|
||||||
|
const compareAtPrice =
|
||||||
|
original !== null && price !== null && original > price
|
||||||
|
? original
|
||||||
|
: null;
|
||||||
|
|
||||||
|
const available =
|
||||||
|
!variant ||
|
||||||
|
variant.allow_backorder === true ||
|
||||||
|
variant.manage_inventory === false ||
|
||||||
|
(variant.inventory_quantity ?? 0) > 0;
|
||||||
|
|
||||||
|
const tags = (product.tags || [])
|
||||||
|
.map((tag) => tag.value || "")
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: product.external_id || product.id,
|
||||||
|
slug,
|
||||||
|
name: product.title || "Producto ARI Shopping",
|
||||||
|
shortDescription:
|
||||||
|
product.subtitle ||
|
||||||
|
product.title ||
|
||||||
|
"Producto ARI Shopping",
|
||||||
|
description:
|
||||||
|
product.description ||
|
||||||
|
"Producto disponible para confirmación de inventario con ARI Shopping.",
|
||||||
|
category,
|
||||||
|
collection,
|
||||||
|
brand,
|
||||||
|
ageRange,
|
||||||
|
price,
|
||||||
|
compareAtPrice,
|
||||||
|
currency: normalizeCurrency(calculated?.currency_code),
|
||||||
|
priceLabel: price === null ? "Consultar disponibilidad" : "",
|
||||||
|
available,
|
||||||
|
featured:
|
||||||
|
booleanMetadata(product.metadata, "featured") ||
|
||||||
|
legacy?.featured ||
|
||||||
|
false,
|
||||||
|
isNew:
|
||||||
|
booleanMetadata(product.metadata, "isNew") ||
|
||||||
|
legacy?.isNew ||
|
||||||
|
false,
|
||||||
|
image: images[0] || thumbnail,
|
||||||
|
thumbnail,
|
||||||
|
images: images.length > 0 ? images : [thumbnail],
|
||||||
|
tags,
|
||||||
|
variants: (product.variants || []).map(mapVariant),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function matchesFilters(
|
||||||
|
product: Product,
|
||||||
|
filters?: ProductFilters,
|
||||||
|
): boolean {
|
||||||
|
if (!filters) return true;
|
||||||
|
|
||||||
|
const query = filters.query?.trim().toLocaleLowerCase("es");
|
||||||
|
if (
|
||||||
|
query &&
|
||||||
|
![
|
||||||
|
product.name,
|
||||||
|
product.shortDescription,
|
||||||
|
product.description,
|
||||||
|
product.category,
|
||||||
|
product.collection,
|
||||||
|
product.brand,
|
||||||
|
...product.tags,
|
||||||
|
]
|
||||||
|
.join(" ")
|
||||||
|
.toLocaleLowerCase("es")
|
||||||
|
.includes(query)
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
filters.category &&
|
||||||
|
filters.category !== "Todos" &&
|
||||||
|
product.category !== filters.category
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
filters.collection &&
|
||||||
|
product.collection !== filters.collection
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
filters.brand &&
|
||||||
|
filters.brand !== "Todas" &&
|
||||||
|
product.brand !== filters.brand
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
filters.ageRange &&
|
||||||
|
filters.ageRange !== "Todas" &&
|
||||||
|
product.ageRange !== filters.ageRange
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
typeof filters.minPrice === "number" &&
|
||||||
|
product.price !== null &&
|
||||||
|
product.price < filters.minPrice
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
typeof filters.maxPrice === "number" &&
|
||||||
|
product.price !== null &&
|
||||||
|
product.price > filters.maxPrice
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchProducts(
|
||||||
|
filters?: ProductFilters,
|
||||||
|
limit = 100,
|
||||||
|
): Promise<Product[]> {
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
limit: String(limit),
|
||||||
|
currency_code: "cop",
|
||||||
|
fields:
|
||||||
|
"+metadata,+images,+tags,+categories,+collection,+variants.inventory_quantity,+variants.calculated_price,+variants.options",
|
||||||
|
});
|
||||||
|
|
||||||
|
if (filters?.query) {
|
||||||
|
params.set("q", filters.query);
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await medusaFetch<MedusaProductsResponse>(
|
||||||
|
"/store/products",
|
||||||
|
params,
|
||||||
|
);
|
||||||
|
|
||||||
|
return response.products
|
||||||
|
.map(mapProduct)
|
||||||
|
.filter((product) => matchesFilters(product, filters));
|
||||||
}
|
}
|
||||||
|
|
||||||
export const medusaCatalogProvider: CatalogProvider = {
|
export const medusaCatalogProvider: CatalogProvider = {
|
||||||
async getProducts() {
|
async getProducts(filters) {
|
||||||
await medusaFetch<unknown>("/store/products?limit=24");
|
return fetchProducts(filters);
|
||||||
return notImplemented();
|
|
||||||
},
|
},
|
||||||
async getProductBySlug(): Promise<Product | null> {
|
|
||||||
return notImplemented();
|
async getProductBySlug(slug) {
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
handle: slug,
|
||||||
|
limit: "1",
|
||||||
|
currency_code: "cop",
|
||||||
|
fields:
|
||||||
|
"+metadata,+images,+tags,+categories,+collection,+variants.inventory_quantity,+variants.calculated_price,+variants.options",
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await medusaFetch<MedusaProductsResponse>(
|
||||||
|
"/store/products",
|
||||||
|
params,
|
||||||
|
);
|
||||||
|
|
||||||
|
const product = response.products[0];
|
||||||
|
return product ? mapProduct(product) : null;
|
||||||
},
|
},
|
||||||
async searchProducts(): Promise<Product[]> {
|
|
||||||
return notImplemented();
|
async searchProducts(query, limit = 8) {
|
||||||
|
return fetchProducts({ query }, limit);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -10,6 +10,24 @@ const nextConfig: NextConfig = {
|
|||||||
minimumCacheTTL: 604800,
|
minimumCacheTTL: 604800,
|
||||||
deviceSizes: [360, 640, 768, 1024, 1280, 1536],
|
deviceSizes: [360, 640, 768, 1024, 1280, 1536],
|
||||||
imageSizes: [64, 96, 160, 256, 384, 560],
|
imageSizes: [64, 96, 160, 256, 384, 560],
|
||||||
|
|
||||||
|
remotePatterns: [
|
||||||
|
{
|
||||||
|
protocol: "https",
|
||||||
|
hostname: "media.cruzcloud.net",
|
||||||
|
pathname: "/ari-products/**",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
protocol: "https",
|
||||||
|
hostname: "shop.cruzcloud.net",
|
||||||
|
pathname: "/images/products/**",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
protocol: "https",
|
||||||
|
hostname: "shop.cruzcloud.net",
|
||||||
|
pathname: "/images/thumbnails/**",
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
|
|
||||||
async headers() {
|
async headers() {
|
||||||
@@ -28,4 +46,4 @@ const nextConfig: NextConfig = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default nextConfig;
|
export default nextConfig;
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: frontend-deploy
|
||||||
|
spec:
|
||||||
|
template:
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: web
|
||||||
|
env:
|
||||||
|
- name: HEADLESS_PROVIDER
|
||||||
|
value: medusa
|
||||||
|
|
||||||
|
- name: MEDUSA_BACKEND_URL
|
||||||
|
value: http://medusa-svc:9000
|
||||||
|
|
||||||
|
- name: MEDUSA_PUBLISHABLE_KEY
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: commerce-storefront
|
||||||
|
key: MEDUSA_PUBLISHABLE_KEY
|
||||||
Reference in New Issue
Block a user