diff --git a/PRODUCTION_DEPLOYMENT_GUIDE.md b/PRODUCTION_DEPLOYMENT_GUIDE.md index 08eb9d93..83c0b8d4 100644 --- a/PRODUCTION_DEPLOYMENT_GUIDE.md +++ b/PRODUCTION_DEPLOYMENT_GUIDE.md @@ -1388,50 +1388,40 @@ kubectl get pods -n bakery-ia -l app.kubernetes.io/instance=signoz ### Step 7.5: Deploy Kubernetes Infrastructure Monitoring (Required for SigNoz Infrastructure View) -> **Purpose:** Deploy kube-state-metrics and node-exporter to enable Kubernetes infrastructure metrics in SigNoz. Without these components, the SigNoz Infrastructure section will be empty. +> **Purpose:** Deploy the official SigNoz k8s-infra chart to enable comprehensive Kubernetes infrastructure metrics in SigNoz. This replaces the need for separate kube-state-metrics and node-exporter deployments. ❌ Removed legacy components: kube-state-metrics and node-exporter. **Components Deployed:** | Component | Purpose | Metrics | |-----------|---------|---------| -| **kube-state-metrics** | Kubernetes object metrics | Pods, Deployments, Nodes, PVCs, etc. | -| **node-exporter** | Host-level metrics | CPU, Memory, Disk, Network | +| **SigNoz k8s-infra** | Unified Kubernetes infrastructure monitoring | Host metrics (CPU, Memory, Disk, Network), Kubelet metrics (Pod/container usage), Cluster metrics (Deployments, Pods, Nodes), Kubernetes events | -**Deploy using the automated script:** +**Deploy using the official SigNoz k8s-infra chart:** ```bash -# Navigate to the k8s-infra monitoring directory -cd /root/bakery-ia +# Add SigNoz Helm repository (if not already added) +helm repo add signoz https://charts.signoz.io +helm repo update -# Make the script executable (if not already) -chmod +x infrastructure/monitoring/k8s-infra/deploy-k8s-infra-monitoring.sh - -# Deploy kube-state-metrics and node-exporter -./infrastructure/monitoring/k8s-infra/deploy-k8s-infra-monitoring.sh --microk8s install -``` - -**Upgrade SigNoz to scrape the new metrics:** - -```bash -# The signoz-values-prod.yaml already includes the Prometheus receiver configuration -# Upgrade SigNoz to apply the scraping configuration -microk8s helm3 upgrade signoz signoz/signoz \ +# Install the k8s-infra chart +helm upgrade --install k8s-infra signoz/k8s-infra \ -n bakery-ia \ - -f infrastructure/monitoring/signoz/signoz-values-prod.yaml + -f infrastructure/monitoring/signoz/k8s-infra-values-prod.yaml \ + --timeout 10m + +# Wait for the DaemonSet to be ready +kubectl wait --for=condition=ready pod -l app.kubernetes.io/name=signoz-agent -n bakery-ia --timeout=300s ``` -**Verify deployment:** +**Verify k8s-infra deployment:** ```bash -# Check pods are running -microk8s kubectl get pods -n bakery-ia | grep -E "(kube-state|node-exporter)" +# Check if the k8s-infra agent is running (should see one pod per node) +kubectl get pods -n bakery-ia -l app.kubernetes.io/name=signoz-agent -# Expected output: -# kube-state-metrics-xxxxxxxxxx-xxxxx 1/1 Running 0 1m -# node-exporter-prometheus-node-exporter-xxxxx 1/1 Running 0 1m - -# Check status -./infrastructure/monitoring/k8s-infra/deploy-k8s-infra-monitoring.sh --microk8s status +# Expected output (one pod per cluster node): +# signoz-agent-xxxxx 1/1 Running 0 1m +# signoz-agent-yyyyy 1/1 Running 0 1m ``` **Verify metrics in SigNoz:** @@ -1440,22 +1430,34 @@ After a few minutes, you should see: - **Infrastructure → Kubernetes**: Pod status, deployments, nodes, PVCs - **Infrastructure → Hosts**: CPU, memory, disk, network usage +**Important Notes:** + +1. **Legacy Components Removal:** If you previously had kube-state-metrics or node-exporter deployed, you should remove them to avoid duplicate metrics: + ```bash + # Remove legacy components if they exist + helm uninstall kube-state-metrics -n bakery-ia 2>/dev/null || true + helm uninstall node-exporter-prometheus-node-exporter -n bakery-ia 2>/dev/null || true + ``` + +2. **Configuration:** The k8s-infra chart is configured via `k8s-infra-values-prod.yaml` which specifies: + - Connection to your SigNoz OTel collector endpoint + - Collection intervals and presets for different metric types + - Resource limits for the monitoring agents + **Troubleshooting:** ```bash -# Check if metrics are being scraped -microk8s kubectl port-forward svc/kube-state-metrics 8080:8080 -n bakery-ia & -curl localhost:8080/metrics | head -20 +# Check k8s-infra agent logs +kubectl logs -l app.kubernetes.io/name=signoz-agent -n bakery-ia --tail=50 -# Check OTel Collector logs for scraping errors -microk8s kubectl logs -l app.kubernetes.io/name=signoz-otel-collector -n bakery-ia --tail=50 +# Verify the agent can connect to SigNoz collector +kubectl logs -l app.kubernetes.io/name=signoz-agent -n bakery-ia | grep -i error ``` > **Files Location:** -> - Helm values: `infrastructure/monitoring/k8s-infra/kube-state-metrics-values.yaml` -> - Helm values: `infrastructure/monitoring/k8s-infra/node-exporter-values.yaml` -> - Deploy script: `infrastructure/monitoring/k8s-infra/deploy-k8s-infra-monitoring.sh` -> - Documentation: `infrastructure/monitoring/k8s-infra/README.md` +> - Helm values: `infrastructure/monitoring/signoz/k8s-infra-values-prod.yaml` +> - Helm values: `infrastructure/monitoring/signoz/k8s-infra-values-dev.yaml` +> - Documentation: `infrastructure/monitoring/signoz/README.md` --- @@ -1528,30 +1530,119 @@ kubectl exec -n bakery-ia deployment/redis -- redis-cli ping ### Configure Stripe Keys (Required Before Going Live) -Before accepting payments, configure your Stripe credentials: +**IMPORTANT**: Before going live, you MUST replace test keys with live Stripe keys. + +#### Step 1: Get Your Live Stripe Keys + +1. Go to [Stripe Dashboard](https://dashboard.stripe.com/apikeys) +2. Make sure you're in **Live mode** (toggle in top right) +3. Copy your **Publishable key** (starts with `pk_live_`) +4. Copy your **Secret key** (starts with `sk_live_`) +5. Get your **Webhook signing secret** from Stripe webhook settings + +#### Step 2: Update Configuration Files ```bash -# Edit ConfigMap for publishable key +# 1. Update the common configmap with your live publishable key nano infrastructure/environments/common/configs/configmap.yaml -# Add: VITE_STRIPE_PUBLISHABLE_KEY: "pk_live_XXXXXXXXXXXX" -# Encode your secret keys -echo -n "sk_live_XXXXXXXXXX" | base64 # Your secret key -echo -n "whsec_XXXXXXXXXX" | base64 # Your webhook secret +# Find and replace these lines: +VITE_STRIPE_PUBLISHABLE_KEY: "pk_test_51QuxKyIzCdnBmAVTGM8fvXYkItrBUILz6lHYwhAva6ZAH1HRi0e8zDRgZ4X3faN0zEABp5RHjCVBmMJL3aKXbaC200fFrSNnPl" +VITE_STRIPE_ACCOUNT_ID: "acct_1QuxKsIucMC6K1cg" -# Edit Secrets +# Replace with your live key and account ID: +VITE_STRIPE_PUBLISHABLE_KEY: "pk_live_your_publishable_key_here" +VITE_STRIPE_ACCOUNT_ID: "acct_1QuxKsIucMC6K1cg" # Keep your account ID, just remove "test_" prefix if needed + +# 2. Encode your live secret keys (required for Kubernetes secrets) +echo -n "sk_live_your_secret_key_here" | base64 +# Example output: c2tfbGl2ZV95b3VyX3NlY3JldF9rZXlfaGVyZQ== + +echo -n "whsec_your_webhook_secret_here" | base64 +# Example output: d2hzZWNfeW91cl93ZWJob29rX3NlY3JldF9oZXJl + +# 3. Update the secrets file nano infrastructure/environments/common/configs/secrets.yaml -# Add to payment-secrets section: -# STRIPE_SECRET_KEY: -# STRIPE_WEBHOOK_SECRET: -# Apply the updated configuration -kubectl apply -k infrastructure/environments/prod/k8s-manifests +# Find the payment-secrets section and update: +STRIPE_SECRET_KEY: c2tfbGl2ZV95b3VyX3NlY3JldF9rZXlfaGVyZQ== # Replace with your encoded live secret key +STRIPE_WEBHOOK_SECRET: d2hzZWNfeW91cl93ZWJob29rX3NlY3JldF9oZXJl # Replace with your encoded webhook secret -# Restart services that use Stripe -kubectl rollout restart deployment/payment-service -n bakery-ia +# 4. Update production kustomization +nano infrastructure/environments/prod/k8s-manifests/kustomization.yaml + +# Find and update the Stripe configuration patch: +- op: replace + path: /data/VITE_STRIPE_PUBLISHABLE_KEY + value: "pk_live_your_publishable_key_here" +- op: add + path: /data/VITE_STRIPE_ACCOUNT_ID + value: "acct_1QuxKsIucMC6K1cg" ``` +#### Step 3: Apply Configuration and Restart Services + +```bash +# Apply the updated configuration +kubectl apply -k infrastructure/environments/prod/k8s-manifests/ + +# Restart services that use Stripe (order matters) +kubectl rollout restart deployment/tenant-service -n bakery-ia +kubectl rollout restart deployment/gateway -n bakery-ia +kubectl rollout restart deployment/frontend -n bakery-ia + +# Monitor the restart process +kubectl get pods -n bakery-ia -w +``` + +#### Step 4: Verify Stripe Configuration + +```bash +# Check that the configmap was updated correctly +kubectl get configmap bakery-config -n bakery-ia -o yaml | grep STRIPE + +# Check that secrets are properly encoded +kubectl get secret payment-secrets -n bakery-ia -o yaml | grep STRIPE + +# Test a small payment (€1.00) with a real card +# Use Stripe test cards first: 4242 4242 4242 4242 +``` + +#### Step 5: Update Stripe Webhooks (Critical) + +```bash +# 1. Update your Stripe webhook endpoint to use the live URL: +# https://bakewise.ai/api/webhooks/stripe + +# 2. Update the webhook signing secret in Stripe dashboard +# to match what you configured in secrets.yaml + +# 3. Test webhooks: +stripe trigger payment_intent.succeeded +stripe trigger invoice.paid +``` + +#### Step 6: PCI Compliance Checklist + +Before going live, ensure: +- [ ] All payment pages use HTTPS (check your ingress TLS configuration) +- [ ] No card data is logged or stored in your databases +- [ ] Your server meets PCI DSS requirements +- [ ] You have a vulnerability management process +- [ ] Regular security audits are scheduled + +#### Step 7: Go Live Checklist + +- [ ] Stripe live keys configured in all services +- [ ] Webhooks tested and working +- [ ] PCI compliance verified +- [ ] Test payments successful in live mode +- [ ] Refund process tested +- [ ] Customer support ready for payment issues +- [ ] Monitoring set up for payment failures + +**WARNING**: Once you switch to live keys, real money will be processed. Start with small test transactions and monitor closely. + ### Backup Strategy ```bash @@ -1730,3 +1821,79 @@ This guide provides a complete, step-by-step process for deploying Bakery-IA to 4. **Scalable:** Designed for 10-100+ tenants with clear scaling path For questions or issues, refer to the troubleshooting guide or consult the support resources listed above. + +### Email System Configuration + +#### Setting Up email-secrets Properly + +**Important:** The `email-secrets` must be configured to use the Mailu admin account credentials for proper email functionality. + +**Recommended Approach:** + +1. **Use Mailu Admin Account** (instead of creating separate postmaster account): + +```bash +# Get the admin password from mailu-admin-credentials +ADMIN_PASSWORD=$(kubectl get secret mailu-admin-credentials -n bakery-ia -o jsonpath='{.data.password}' | base64 -d) + +# Update email-secrets to use admin account +kubectl edit secret email-secrets -n bakery-ia + +# Change the values to: +# SMTP_USER: admin@bakewise.ai +# SMTP_PASSWORD: [the admin password you retrieved] +``` + +2. **Alternative: Create Postmaster Account** (if you prefer separate accounts): + +```bash +# Log in to Mailu admin panel +# URL: https://mail.bakewise.ai/admin +# Username: admin@bakewise.ai +# Password: [from mailu-admin-credentials] + +# Navigate to Users -> Create New User +# Email: postmaster@bakewise.ai +# Password: [generate secure password] +# Role: Admin (or create custom role with email sending permissions) + +# Update email-secrets with the postmaster credentials +kubectl edit secret email-secrets -n bakery-ia +``` + +**Verifying Email Configuration:** + +```bash +# Test email sending via notification service +kubectl exec -n bakery-ia deployment/notification-service -it -- bash + +# Inside the container: +python -c " +from app.services.email_service import EmailService +from app.core.config import settings +es = EmailService() +print('Testing email service...') +result = await es.health_check() +print(f'Email service healthy: {result}') +" +``` + +**Troubleshooting Email Issues:** + +```bash +# Check Mailu logs +kubectl logs -n bakery-ia deployment/mailu-postfix | tail -50 + +# Check notification service logs +kubectl logs -n bakery-ia deployment/notification-service | grep -i email | tail -20 + +# Test SMTP connection manually +kubectl run -it --rm smtp-test --image=alpine -- + apk add openssl && + openssl s_client -connect mailu-postfix:587 -starttls smtp +``` + +**DOVEADM_PASSWORD Note:** +- This is for IMAP administration (rarely used) +- Only needed if you require advanced mailbox management +- Can be safely removed if not using IMAP admin features diff --git a/frontend/package-lock.json b/frontend/package-lock.json index b406c226..14d67a76 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -148,7 +148,6 @@ "integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", @@ -2278,6 +2277,7 @@ "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-2.3.6.tgz", "integrity": "sha512-HJnTFeRM2kVFVr5gr5kH1XP6K0JcJtE7Lzvtr3FS/so5f1kpsqqqxy5JF+FRaO6H2qmcMfAUIox7AJteieRtVw==", "license": "MIT", + "peer": true, "dependencies": { "@formatjs/fast-memoize": "2.2.7", "@formatjs/intl-localematcher": "0.6.2", @@ -2290,6 +2290,7 @@ "resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-2.2.7.tgz", "integrity": "sha512-Yabmi9nSvyOMrlSeGGWDiH7rf3a7sIwplbvo/dlz9WCIjzIQAfy1RMf4S0X3yG724n5Ghu2GmEl5NJIV6O9sZQ==", "license": "MIT", + "peer": true, "dependencies": { "tslib": "^2.8.0" } @@ -2299,6 +2300,7 @@ "resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.11.4.tgz", "integrity": "sha512-7kR78cRrPNB4fjGFZg3Rmj5aah8rQj9KPzuLsmcSn4ipLXQvC04keycTI1F7kJYDwIXtT2+7IDEto842CfZBtw==", "license": "MIT", + "peer": true, "dependencies": { "@formatjs/ecma402-abstract": "2.3.6", "@formatjs/icu-skeleton-parser": "1.8.16", @@ -2310,6 +2312,7 @@ "resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.8.16.tgz", "integrity": "sha512-H13E9Xl+PxBd8D5/6TVUluSpxGNvFSlN/b3coUp0e0JpuWXXnQDiavIpY3NnvSp4xhEMoXyyBvVfdFX8jglOHQ==", "license": "MIT", + "peer": true, "dependencies": { "@formatjs/ecma402-abstract": "2.3.6", "tslib": "^2.8.0" @@ -2320,6 +2323,7 @@ "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.6.2.tgz", "integrity": "sha512-XOMO2Hupl0wdd172Y06h6kLpBz6Dv+J4okPLl4LPtzbr8f66WbIoy4ev98EBuZ6ZK4h5ydTN6XneT4QVpD7cdA==", "license": "MIT", + "peer": true, "dependencies": { "tslib": "^2.8.0" } @@ -2748,7 +2752,6 @@ "deprecated": "Glob versions prior to v9 are no longer supported", "dev": true, "license": "ISC", - "peer": true, "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -2988,7 +2991,6 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", "license": "Apache-2.0", - "peer": true, "engines": { "node": ">=8.0.0" } @@ -6329,7 +6331,6 @@ "resolved": "https://registry.npmjs.org/@stripe/stripe-js/-/stripe-js-4.10.0.tgz", "integrity": "sha512-KrMOL+sH69htCIXCaZ4JluJ35bchuCCznyPyrbN8JXSGQfwBI1SuIEMZNwvy8L8ykj29t6sa5BAAiL7fNoLZ8A==", "license": "MIT", - "peer": true, "engines": { "node": ">=12.16" } @@ -6419,7 +6420,6 @@ "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.89.0.tgz", "integrity": "sha512-SXbtWSTSRXyBOe80mszPxpEbaN4XPRUp/i0EfQK1uyj3KCk/c8FuPJNIRwzOVe/OU3rzxrYtiNabsAmk1l714A==", "license": "MIT", - "peer": true, "dependencies": { "@tanstack/query-core": "5.89.0" }, @@ -6911,7 +6911,6 @@ "integrity": "sha512-0dLEBsA1kI3OezMBF8nSsb7Nk19ZnsyE1LLhB8r27KbgU5H4pvuqZLdtE+aUkJVoXgTVuA+iLIwmZ0TuK4tx6A==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.0.2" @@ -6923,7 +6922,6 @@ "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", "devOptional": true, "license": "MIT", - "peer": true, "peerDependencies": { "@types/react": "^18.0.0" } @@ -7065,7 +7063,6 @@ "integrity": "sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==", "dev": true, "license": "BSD-2-Clause", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "6.21.0", "@typescript-eslint/types": "6.21.0", @@ -7404,7 +7401,6 @@ "integrity": "sha512-xa57bCPGuzEFqGjPs3vVLyqareG8DX0uMkr5U/v5vLv5/ZUrBrPL7gzxzTJedEyZxFMfsozwTIbbYfEQVo3kgg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@vitest/utils": "1.6.1", "fast-glob": "^3.3.2", @@ -7502,7 +7498,6 @@ "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -8045,7 +8040,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.3", "caniuse-lite": "^1.0.30001741", @@ -8251,7 +8245,6 @@ "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.0.tgz", "integrity": "sha512-aYeC/jDgSEx8SHWZvANYMioYMZ2KX02W6f6uVfyteuCGcadDLcYVHdfdygsTQkQ4TKn5lghoojAsPj5pu0SnvQ==", "license": "MIT", - "peer": true, "dependencies": { "@kurkle/color": "^0.3.0" }, @@ -8616,8 +8609,7 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/d3-array": { "version": "3.2.4", @@ -8799,7 +8791,6 @@ "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==", "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.21.0" }, @@ -8842,7 +8833,8 @@ "version": "10.6.0", "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/decimal.js-light": { "version": "2.5.1", @@ -9326,7 +9318,6 @@ "dev": true, "hasInstallScript": true, "license": "MIT", - "peer": true, "bin": { "esbuild": "bin/esbuild" }, @@ -9430,7 +9421,6 @@ "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -10856,7 +10846,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.23.2" } @@ -10905,7 +10894,6 @@ "resolved": "https://registry.npmjs.org/immer/-/immer-10.1.3.tgz", "integrity": "sha512-tmjF/k8QDKydUlm3mZU+tjM6zeq9/fFpPqH9SzWmBnVVKsPBg/V66qsMwb3/Bo90cgUN+ghdVBess+hPsxUyRw==", "license": "MIT", - "peer": true, "funding": { "type": "opencollective", "url": "https://opencollective.com/immer" @@ -11887,8 +11875,7 @@ "version": "1.9.4", "resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz", "integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==", - "license": "BSD-2-Clause", - "peer": true + "license": "BSD-2-Clause" }, "node_modules/leven": { "version": "3.1.0", @@ -13077,7 +13064,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -13244,7 +13230,6 @@ "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", "dev": true, "license": "MIT", - "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -13541,7 +13526,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -13614,7 +13598,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -13680,7 +13663,6 @@ "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.63.0.tgz", "integrity": "sha512-ZwueDMvUeucovM2VjkCf7zIHcs1aAlDimZu2Hvel5C5907gUzMpm4xCrQXtRzCvsBqFjonB4m3x4LzCFI1ZKWA==", "license": "MIT", - "peer": true, "engines": { "node": ">=18.0.0" }, @@ -14286,7 +14268,6 @@ "integrity": "sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ==", "dev": true, "license": "MIT", - "peer": true, "bin": { "rollup": "dist/bin/rollup" }, @@ -15169,7 +15150,6 @@ "integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", @@ -15701,7 +15681,6 @@ "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -16113,7 +16092,6 @@ "integrity": "sha512-j3lYzGC3P+B5Yfy/pfKNgVEg4+UtcIJcVRt2cDjIOmhLourAqPqf8P7acgxeiSgUB7E3p2P8/3gNIgDLpwzs4g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", @@ -16677,7 +16655,6 @@ "integrity": "sha512-Ljb1cnSJSivGN0LqXd/zmDbWEM0RNNg2t1QW/XUhYl/qPqyu7CsqeWtqQXHVaJsecLPuDoak2oJcZN2QoRIOag==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@vitest/expect": "1.6.1", "@vitest/runner": "1.6.1", @@ -17059,7 +17036,6 @@ "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", diff --git a/frontend/src/api/client/apiClient.ts b/frontend/src/api/client/apiClient.ts index 51d607b9..d64bf4df 100644 --- a/frontend/src/api/client/apiClient.ts +++ b/frontend/src/api/client/apiClient.ts @@ -79,6 +79,9 @@ class ApiClient { const publicEndpoints = [ '/demo/accounts', '/demo/session/create', + '/public/contact', + '/public/feedback', + '/public/prelaunch-subscribe', ]; // Endpoints that require authentication but not a tenant ID (user-level endpoints) diff --git a/frontend/src/api/services/publicContact.ts b/frontend/src/api/services/publicContact.ts new file mode 100644 index 00000000..bac2290e --- /dev/null +++ b/frontend/src/api/services/publicContact.ts @@ -0,0 +1,83 @@ +/** + * Public Contact API Service + * Handles public form submissions (contact, feedback, prelaunch) + * These endpoints don't require authentication + */ + +import axios from 'axios'; +import { getApiUrl } from '../../config/runtime'; + +const publicApiClient = axios.create({ + baseURL: getApiUrl(), + timeout: 30000, + headers: { + 'Content-Type': 'application/json', + }, +}); + +// Types +export interface ContactFormData { + name: string; + email: string; + phone?: string; + bakery_name?: string; + type: 'general' | 'technical' | 'sales' | 'feedback'; + subject: string; + message: string; +} + +export interface FeedbackFormData { + name: string; + email: string; + category: 'suggestion' | 'bug' | 'feature' | 'praise' | 'complaint'; + title: string; + description: string; + rating?: number; +} + +export interface PrelaunchEmailData { + email: string; +} + +export interface ContactFormResponse { + success: boolean; + message: string; +} + +// API Functions +export const publicContactService = { + /** + * Submit a contact form + */ + submitContactForm: async (data: ContactFormData): Promise => { + const response = await publicApiClient.post( + '/v1/public/contact', + data + ); + return response.data; + }, + + /** + * Submit a feedback form + */ + submitFeedbackForm: async (data: FeedbackFormData): Promise => { + const response = await publicApiClient.post( + '/v1/public/feedback', + data + ); + return response.data; + }, + + /** + * Submit a prelaunch email subscription + */ + submitPrelaunchEmail: async (data: PrelaunchEmailData): Promise => { + const response = await publicApiClient.post( + '/v1/public/prelaunch-subscribe', + data + ); + return response.data; + }, +}; + +export default publicContactService; diff --git a/frontend/src/components/domain/auth/PrelaunchEmailForm.tsx b/frontend/src/components/domain/auth/PrelaunchEmailForm.tsx new file mode 100644 index 00000000..036487b0 --- /dev/null +++ b/frontend/src/components/domain/auth/PrelaunchEmailForm.tsx @@ -0,0 +1,185 @@ +import React, { useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { Mail, Rocket, CheckCircle, Loader, ArrowLeft } from 'lucide-react'; +import { Button, Input, Card } from '../../ui'; +import { publicContactService } from '../../../api/services/publicContact'; + +interface PrelaunchEmailFormProps { + onLoginClick?: () => void; + className?: string; +} + +export const PrelaunchEmailForm: React.FC = ({ + onLoginClick, + className = '', +}) => { + const { t } = useTranslation(['auth', 'common']); + const [email, setEmail] = useState(''); + const [isSubmitting, setIsSubmitting] = useState(false); + const [isSubmitted, setIsSubmitted] = useState(false); + const [error, setError] = useState(null); + + const validateEmail = (email: string): boolean => { + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + return emailRegex.test(email); + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setError(null); + + if (!email.trim()) { + setError(t('auth:prelaunch.email_required')); + return; + } + + if (!validateEmail(email)) { + setError(t('auth:prelaunch.email_invalid')); + return; + } + + setIsSubmitting(true); + + try { + await publicContactService.submitPrelaunchEmail({ email }); + setIsSubmitted(true); + } catch { + setError(t('auth:prelaunch.submit_error')); + } finally { + setIsSubmitting(false); + } + }; + + if (isSubmitted) { + return ( + +
+
+ +
+ +

+ {t('auth:prelaunch.success_title')} +

+ +

+ {t('auth:prelaunch.success_message')} +

+ +
+ + + {onLoginClick && ( +

+ {t('auth:register.have_account')}{' '} + +

+ )} +
+
+
+ ); + } + + return ( + +
+
+ +
+ +

+ {t('auth:prelaunch.title')} +

+ +

+ {t('auth:prelaunch.subtitle')} +

+ +

+ {t('auth:prelaunch.description')} +

+
+ +
+ setEmail(e.target.value)} + leftIcon={} + error={error || undefined} + isRequired + size="lg" + /> + + + + +
+

+ {t('auth:prelaunch.benefits_title')} +

+
    +
  • + + {t('auth:prelaunch.benefit_1')} +
  • +
  • + + {t('auth:prelaunch.benefit_2')} +
  • +
  • + + {t('auth:prelaunch.benefit_3')} +
  • +
+
+ + {onLoginClick && ( +
+

+ {t('auth:register.have_account')}{' '} + +

+
+ )} +
+ ); +}; + +export default PrelaunchEmailForm; diff --git a/frontend/src/components/domain/auth/RegistrationContainer.tsx b/frontend/src/components/domain/auth/RegistrationContainer.tsx index b00429d1..cbe4ac0b 100644 --- a/frontend/src/components/domain/auth/RegistrationContainer.tsx +++ b/frontend/src/components/domain/auth/RegistrationContainer.tsx @@ -29,7 +29,12 @@ const getStripeKey = (): string => { return import.meta.env.VITE_STRIPE_PUBLISHABLE_KEY || 'pk_test_51234567890123456789012345678901234567890123456789012345678901234567890123456789012345'; }; -const stripePromise = loadStripe(getStripeKey()); +// Force Stripe to use test environment by loading from test endpoint +const stripePromise = loadStripe(getStripeKey(), { + stripeAccount: import.meta.env.VITE_STRIPE_ACCOUNT_ID, + apiVersion: '2023-10-16', + betas: ['elements_v2'] +}); interface RegistrationContainerProps { onSuccess?: () => void; diff --git a/frontend/src/components/domain/auth/index.ts b/frontend/src/components/domain/auth/index.ts index 2b19a93f..57f73296 100644 --- a/frontend/src/components/domain/auth/index.ts +++ b/frontend/src/components/domain/auth/index.ts @@ -3,13 +3,15 @@ export { default as LoginForm } from './LoginForm'; export { default as RegistrationContainer } from './RegistrationContainer'; export { default as PasswordResetForm } from './PasswordResetForm'; export { default as ProfileSettings } from './ProfileSettings'; +export { default as PrelaunchEmailForm } from './PrelaunchEmailForm'; // Re-export types for convenience -export type { +export type { LoginFormProps, - RegistrationContainerProps, + RegistrationContainerProps, PasswordResetFormProps, - ProfileSettingsProps + ProfileSettingsProps, + PrelaunchEmailFormProps } from './types'; // Component metadata for documentation diff --git a/frontend/src/components/domain/auth/types.ts b/frontend/src/components/domain/auth/types.ts index 66463355..4540a2c9 100644 --- a/frontend/src/components/domain/auth/types.ts +++ b/frontend/src/components/domain/auth/types.ts @@ -29,6 +29,11 @@ export interface ProfileSettingsProps { initialTab?: 'profile' | 'security' | 'preferences' | 'notifications'; } +export interface PrelaunchEmailFormProps { + onLoginClick?: () => void; + className?: string; +} + // Additional types for internal use export type RegistrationStep = 'personal' | 'bakery' | 'security' | 'verification'; diff --git a/frontend/src/components/subscription/PaymentMethodUpdateModal.tsx b/frontend/src/components/subscription/PaymentMethodUpdateModal.tsx index e5a5e202..03346271 100644 --- a/frontend/src/components/subscription/PaymentMethodUpdateModal.tsx +++ b/frontend/src/components/subscription/PaymentMethodUpdateModal.tsx @@ -52,6 +52,8 @@ const getStripePublishableKey = (): string => { const stripePromise = loadStripe(getStripePublishableKey(), { betas: ['elements_v2'], locale: 'auto', + stripeAccount: import.meta.env.VITE_STRIPE_ACCOUNT_ID, + apiVersion: '2023-10-16' }); /** diff --git a/frontend/src/components/subscription/SubscriptionPricingCards.tsx b/frontend/src/components/subscription/SubscriptionPricingCards.tsx index 8014d93f..eb09e8a6 100644 --- a/frontend/src/components/subscription/SubscriptionPricingCards.tsx +++ b/frontend/src/components/subscription/SubscriptionPricingCards.tsx @@ -11,6 +11,7 @@ import { SUBSCRIPTION_TIERS } from '../../api'; import { getRegisterUrl } from '../../utils/navigation'; +import { PRELAUNCH_CONFIG } from '../../config/prelaunch'; type BillingCycle = 'monthly' | 'yearly'; type DisplayMode = 'landing' | 'settings' | 'selection'; @@ -411,12 +412,16 @@ export const SubscriptionPricingCards: React.FC = ) : mode === 'settings' ? t('ui.change_subscription', 'Cambiar Suscripción') + : PRELAUNCH_CONFIG.enabled + ? t('ui.notify_me', 'Avísame del Lanzamiento') : t('ui.start_free_trial')} {/* Footer */}

- {showPilotBanner + {PRELAUNCH_CONFIG.enabled + ? t('ui.prelaunch_footer', 'Lanzamiento oficial próximamente') + : showPilotBanner ? t('ui.free_trial_footer', { months: pilotTrialMonths }) : t('ui.free_trial_footer', { months: 0 }) } diff --git a/frontend/src/config/prelaunch.ts b/frontend/src/config/prelaunch.ts new file mode 100644 index 00000000..8c81faa6 --- /dev/null +++ b/frontend/src/config/prelaunch.ts @@ -0,0 +1,17 @@ +/** + * Pre-launch Mode Configuration + * Uses build-time environment variables + * + * When VITE_PRELAUNCH_MODE=true: + * - Registration page shows email capture form instead of Stripe flow + * - Pricing cards link to the same page but show interest form + * + * When VITE_PRELAUNCH_MODE=false (or not set): + * - Normal registration flow with Stripe payments + */ + +export const PRELAUNCH_CONFIG = { + enabled: import.meta.env.VITE_PRELAUNCH_MODE === 'false', +}; + +export default PRELAUNCH_CONFIG; diff --git a/frontend/src/locales/en/auth.json b/frontend/src/locales/en/auth.json index 30b9a7a5..bfaf819f 100644 --- a/frontend/src/locales/en/auth.json +++ b/frontend/src/locales/en/auth.json @@ -116,6 +116,23 @@ "secure_payment": "Your payment information is protected with end-to-end encryption", "payment_info_secure": "Your payment information is secure" }, + "prelaunch": { + "title": "Coming Soon", + "subtitle": "We're preparing something special for your bakery", + "description": "Be the first to know when we officially launch. Leave your email and we'll notify you.", + "email_required": "Email is required", + "email_invalid": "Please enter a valid email address", + "submit_error": "An error occurred. Please try again.", + "subscribe_button": "Notify Me", + "submitting": "Submitting...", + "success_title": "You're on the list!", + "success_message": "We'll send you an email when we're ready to launch. Thanks for your interest!", + "back_to_home": "Back to Home", + "benefits_title": "By subscribing you'll receive:", + "benefit_1": "Early access to launch", + "benefit_2": "Exclusive offers for early adopters", + "benefit_3": "Product news and updates" + }, "steps": { "info": "Information", "subscription": "Plan", diff --git a/frontend/src/locales/en/subscription.json b/frontend/src/locales/en/subscription.json index b723f18f..25943c05 100644 --- a/frontend/src/locales/en/subscription.json +++ b/frontend/src/locales/en/subscription.json @@ -157,6 +157,8 @@ "payment_details": "Payment Details", "payment_info_secure": "Your payment information is protected with end-to-end encryption", "updating_payment": "Updating...", - "cancel": "Cancel" + "cancel": "Cancel", + "notify_me": "Notify Me of Launch", + "prelaunch_footer": "Official launch coming soon" } } diff --git a/frontend/src/locales/es/auth.json b/frontend/src/locales/es/auth.json index 1468d0fd..7803ff38 100644 --- a/frontend/src/locales/es/auth.json +++ b/frontend/src/locales/es/auth.json @@ -127,6 +127,23 @@ "go_to_login": "Ir a inicio de sesión", "try_again": "Intentar registro de nuevo" }, + "prelaunch": { + "title": "Próximamente", + "subtitle": "Estamos preparando algo especial para tu panadería", + "description": "Sé el primero en saber cuándo lancemos oficialmente. Déjanos tu email y te avisaremos.", + "email_required": "El correo electrónico es obligatorio", + "email_invalid": "Por favor, introduce un correo electrónico válido", + "submit_error": "Ha ocurrido un error. Por favor, inténtalo de nuevo.", + "subscribe_button": "Quiero que me avisen", + "submitting": "Enviando...", + "success_title": "¡Genial! Te hemos apuntado", + "success_message": "Te enviaremos un email cuando estemos listos para el lanzamiento. ¡Gracias por tu interés!", + "back_to_home": "Volver al inicio", + "benefits_title": "Al suscribirte recibirás:", + "benefit_1": "Acceso anticipado al lanzamiento", + "benefit_2": "Ofertas exclusivas para early adopters", + "benefit_3": "Noticias y actualizaciones del producto" + }, "steps": { "info": "Información", "subscription": "Plan", diff --git a/frontend/src/locales/es/subscription.json b/frontend/src/locales/es/subscription.json index 6027e73e..367c4f68 100644 --- a/frontend/src/locales/es/subscription.json +++ b/frontend/src/locales/es/subscription.json @@ -157,6 +157,8 @@ "payment_details": "Detalles de Pago", "payment_info_secure": "Tu información de pago está protegida con encriptación de extremo a extremo", "updating_payment": "Actualizando...", - "cancel": "Cancelar" + "cancel": "Cancelar", + "notify_me": "Avísame del Lanzamiento", + "prelaunch_footer": "Lanzamiento oficial próximamente" } } diff --git a/frontend/src/pages/public/ContactPage.tsx b/frontend/src/pages/public/ContactPage.tsx index ddb5fead..00b3da70 100644 --- a/frontend/src/pages/public/ContactPage.tsx +++ b/frontend/src/pages/public/ContactPage.tsx @@ -12,6 +12,7 @@ import { AlertCircle, HelpCircle } from 'lucide-react'; +import { publicContactService } from '../../api/services/publicContact'; interface ContactMethod { id: string; @@ -73,25 +74,35 @@ const ContactPage: React.FC = () => { e.preventDefault(); setSubmitStatus('loading'); - // Simulate API call - await new Promise((resolve) => setTimeout(resolve, 1500)); - - // In production, this would be an actual API call - console.log('Form submitted:', formState); - - setSubmitStatus('success'); - setTimeout(() => { - setSubmitStatus('idle'); - setFormState({ - name: '', - email: '', - phone: '', - bakeryName: '', - subject: '', - message: '', - type: 'general', + try { + await publicContactService.submitContactForm({ + name: formState.name, + email: formState.email, + phone: formState.phone || undefined, + bakery_name: formState.bakeryName || undefined, + type: formState.type, + subject: formState.subject, + message: formState.message, }); - }, 3000); + + setSubmitStatus('success'); + setTimeout(() => { + setSubmitStatus('idle'); + setFormState({ + name: '', + email: '', + phone: '', + bakeryName: '', + subject: '', + message: '', + type: 'general', + }); + }, 3000); + } catch (error) { + console.error('Contact form submission error:', error); + setSubmitStatus('error'); + setTimeout(() => setSubmitStatus('idle'), 5000); + } }; return ( diff --git a/frontend/src/pages/public/FeedbackPage.tsx b/frontend/src/pages/public/FeedbackPage.tsx index 2972dc14..8cd5c7f7 100644 --- a/frontend/src/pages/public/FeedbackPage.tsx +++ b/frontend/src/pages/public/FeedbackPage.tsx @@ -13,6 +13,7 @@ import { AlertCircle, Star } from 'lucide-react'; +import { publicContactService } from '../../api/services/publicContact'; interface FeedbackCategory { id: string; @@ -90,24 +91,33 @@ const FeedbackPage: React.FC = () => { e.preventDefault(); setSubmitStatus('loading'); - // Simulate API call - await new Promise((resolve) => setTimeout(resolve, 1500)); - - // In production, this would be an actual API call - console.log('Feedback submitted:', formState); - - setSubmitStatus('success'); - setTimeout(() => { - setSubmitStatus('idle'); - setFormState({ - name: '', - email: '', - category: 'suggestion', - title: '', - description: '', - rating: 0, + try { + await publicContactService.submitFeedbackForm({ + name: formState.name, + email: formState.email, + category: formState.category, + title: formState.title, + description: formState.description, + rating: formState.rating > 0 ? formState.rating : undefined, }); - }, 3000); + + setSubmitStatus('success'); + setTimeout(() => { + setSubmitStatus('idle'); + setFormState({ + name: '', + email: '', + category: 'suggestion', + title: '', + description: '', + rating: 0, + }); + }, 3000); + } catch (error) { + console.error('Feedback form submission error:', error); + setSubmitStatus('error'); + setTimeout(() => setSubmitStatus('idle'), 5000); + } }; const getCategoryColor = (color: string) => { diff --git a/frontend/src/pages/public/RegisterPage.tsx b/frontend/src/pages/public/RegisterPage.tsx index 61eff583..070cfb6e 100644 --- a/frontend/src/pages/public/RegisterPage.tsx +++ b/frontend/src/pages/public/RegisterPage.tsx @@ -1,7 +1,8 @@ import React from 'react'; import { useNavigate } from 'react-router-dom'; -import { RegistrationContainer } from '../../components/domain/auth'; +import { RegistrationContainer, PrelaunchEmailForm } from '../../components/domain/auth'; import { PublicLayout } from '../../components/layout'; +import { PRELAUNCH_CONFIG } from '../../config/prelaunch'; const RegisterPage: React.FC = () => { const navigate = useNavigate(); @@ -14,6 +15,27 @@ const RegisterPage: React.FC = () => { navigate('/login'); }; + // Show prelaunch email form or full registration based on build-time config + if (PRELAUNCH_CONFIG.enabled) { + return ( + + + + ); + } + return ( +# Checks the complete alert flow for a demo session in production + +# Use microk8s kubectl directly to avoid wrapper issues +KUBECTL="microk8s kubectl" + +TENANT_ID="${1:-your-default-tenant-id}" + +# Try different secret names for Redis password +REDIS_PASSWORD=$($KUBECTL get secret redis-credentials -n bakery-ia -o jsonpath='{.data.REDIS_PASSWORD}' 2>/dev/null | base64 -d 2>/dev/null) +if [ -z "$REDIS_PASSWORD" ]; then + REDIS_PASSWORD=$($KUBECTL get secret redis-secret -n bakery-ia -o jsonpath='{.data.REDIS_PASSWORD}' 2>/dev/null | base64 -d 2>/dev/null) +fi +if [ -z "$REDIS_PASSWORD" ]; then + # Try to get from configmap or use default + REDIS_PASSWORD="redis_pass123" +fi + +echo "==========================================" +echo "Alert Flow Health Check for Tenant: $TENANT_ID" +echo "==========================================" + +echo -e "\n=== 1. RabbitMQ Queues ===" +$KUBECTL exec -n bakery-ia deployment/rabbitmq -- \ + rabbitmqctl list_queues name messages consumers 2>/dev/null | grep -E "alert|event" || echo " No alert/event queues found or RabbitMQ not accessible" + +echo -e "\n=== 2. Alert Processor DB Events (last 10) ===" +$KUBECTL exec -n bakery-ia deployment/alert-processor-db -- \ + psql -U alert_processor_user -d alert_processor_db -c \ + "SELECT event_type, priority_score, type_class, status, created_at + FROM events WHERE tenant_id = '$TENANT_ID' + ORDER BY created_at DESC LIMIT 10;" 2>/dev/null || echo " Could not query alert-processor-db" + +echo -e "\n=== 3. Redis SSE Channels ===" +$KUBECTL exec -n bakery-ia deployment/redis -- \ + redis-cli -a "$REDIS_PASSWORD" --no-auth-warning PUBSUB CHANNELS "*" 2>/dev/null | grep -i "$TENANT_ID" || echo " No active SSE channels for this tenant (channels only exist when frontend is connected)" + +echo -e "\n=== 4. Demo Session Status ===" +$KUBECTL exec -n bakery-ia deployment/demo-session-db -- \ + psql -U demo_session_user -d demo_session_db -c \ + "SELECT id, status, virtual_tenant_id, demo_account_type, data_cloned, created_at, cloning_completed_at + FROM demo_sessions + WHERE virtual_tenant_id::text = '$TENANT_ID' OR id::text = '$TENANT_ID' + ORDER BY created_at DESC LIMIT 1;" 2>/dev/null || echo " Could not query demo-session-db" + +echo -e "\n=== 5. Recent Alert Processor Logs ===" +$KUBECTL logs -n bakery-ia deployment/alert-processor --tail=100 2>/dev/null | \ + grep -iE "received|enriched|stored|error|consuming|rabbitmq|sse" | tail -15 || echo " No relevant logs found" + +echo -e "\n=== 6. Gateway SSE Logs ===" +$KUBECTL logs -n bakery-ia deployment/gateway --tail=100 2>/dev/null | \ + grep -iE "sse|pubsub|events_stream" | tail -10 || echo " No SSE logs found" + +echo -e "\n=== 7. Service Health ===" +# Check deployment status directly +for deploy in alert-processor gateway demo-session-service orchestrator-service inventory-service production-service procurement-service rabbitmq redis; do + READY=$($KUBECTL get deployment/$deploy -n bakery-ia -o jsonpath='{.status.readyReplicas}' 2>/dev/null || echo "0") + DESIRED=$($KUBECTL get deployment/$deploy -n bakery-ia -o jsonpath='{.spec.replicas}' 2>/dev/null || echo "?") + if [ -z "$READY" ]; then READY="0"; fi + echo " $deploy: $READY/$DESIRED ready" +done + +echo -e "\n=== 8. RabbitMQ Connection Status ===" +$KUBECTL exec -n bakery-ia deployment/rabbitmq -- \ + rabbitmqctl list_connections name state 2>/dev/null | head -10 || echo " Could not list connections" + +echo -e "\n=== 9. Recent Demo Session Service Logs (enrichment) ===" +$KUBECTL logs -n bakery-ia deployment/demo-session-service --tail=100 2>/dev/null | \ + grep -iE "enrichment|alert|trigger|clone|post_clone" | tail -10 || echo " No relevant logs found" + +echo -e "\n=== 10. Total Events in Alert Processor DB ===" +$KUBECTL exec -n bakery-ia deployment/alert-processor-db -- \ + psql -U alert_processor_user -d alert_processor_db -c \ + "SELECT COUNT(*) as total_events, + COUNT(*) FILTER (WHERE tenant_id = '$TENANT_ID') as tenant_events + FROM events;" 2>/dev/null || echo " Could not query" + +echo -e "\n==========================================" +echo "Health Check Complete" +echo "==========================================" \ No newline at end of file diff --git a/services/notification/app/api/public_contact.py b/services/notification/app/api/public_contact.py new file mode 100644 index 00000000..7f67aea7 --- /dev/null +++ b/services/notification/app/api/public_contact.py @@ -0,0 +1,545 @@ +# ================================================================ +# services/notification/app/api/public_contact.py +# ================================================================ +""" +Public contact form API endpoints +Handles contact, feedback, and prelaunch email submissions +These endpoints are public (no authentication required) +""" + +import structlog +from fastapi import APIRouter, HTTPException, Request +from pydantic import BaseModel, EmailStr, Field +from typing import Optional, Literal +from datetime import datetime + +from app.services.email_service import EmailService +from app.core.config import settings + +logger = structlog.get_logger() + +router = APIRouter(prefix="/api/v1/public", tags=["public-contact"]) + +# ================================================================ +# PYDANTIC MODELS +# ================================================================ + +class ContactFormRequest(BaseModel): + """Contact form submission request""" + name: str = Field(..., min_length=2, max_length=100) + email: EmailStr + phone: Optional[str] = Field(None, max_length=20) + bakery_name: Optional[str] = Field(None, max_length=100) + type: Literal["general", "technical", "sales", "feedback"] = "general" + subject: str = Field(..., min_length=5, max_length=200) + message: str = Field(..., min_length=10, max_length=5000) + + +class FeedbackFormRequest(BaseModel): + """Feedback form submission request""" + name: str = Field(..., min_length=2, max_length=100) + email: EmailStr + category: Literal["suggestion", "bug", "feature", "praise", "complaint"] + title: str = Field(..., min_length=5, max_length=200) + description: str = Field(..., min_length=10, max_length=5000) + rating: Optional[int] = Field(None, ge=1, le=5) + + +class PrelaunchEmailRequest(BaseModel): + """Prelaunch email subscription request""" + email: EmailStr + + +class ContactFormResponse(BaseModel): + """Response for form submissions""" + success: bool + message: str + + +# ================================================================ +# EMAIL TEMPLATES +# ================================================================ + +CONTACT_EMAIL_TEMPLATE_TEXT = """ +Nuevo mensaje de contacto recibido + +============================================== +DATOS DEL CONTACTO +============================================== + +Nombre: {name} +Email: {email} +Teléfono: {phone} +Panadería: {bakery_name} +Tipo de consulta: {type_label} + +============================================== +ASUNTO +============================================== +{subject} + +============================================== +MENSAJE +============================================== +{message} + +============================================== +Recibido: {timestamp} +""" + +CONTACT_EMAIL_TEMPLATE_HTML = """ + + + + + + + +

+
+

Nuevo Mensaje de Contacto

+
+
+
+
+ Nombre: + {name} +
+
+ Email: + {email} +
+
+ Teléfono: + {phone} +
+
+ Panadería: + {bakery_name} +
+
+ Tipo: + {type_label} +
+
+ +
+

{subject}

+

{message}

+
+
+ +
+ + +""" + +FEEDBACK_EMAIL_TEMPLATE_TEXT = """ +Nuevo feedback recibido + +============================================== +DATOS DEL USUARIO +============================================== + +Nombre: {name} +Email: {email} +Categoría: {category_label} +Valoración: {rating_display} + +============================================== +TÍTULO +============================================== +{title} + +============================================== +DESCRIPCIÓN +============================================== +{description} + +============================================== +Recibido: {timestamp} +""" + +FEEDBACK_EMAIL_TEMPLATE_HTML = """ + + + + + + + +
+
+

Nuevo Feedback Recibido

+
+
+
+
+ Nombre: + {name} +
+
+ Email: + {email} +
+
+ Categoría: + {category_label} +
+
+ Valoración: + {rating_display} +
+
+ +
+

{title}

+

{description}

+
+
+ +
+ + +""" + +PRELAUNCH_EMAIL_TEMPLATE_TEXT = """ +Nueva suscripción de pre-lanzamiento + +============================================== +NUEVO INTERESADO +============================================== + +Email: {email} + +El usuario ha mostrado interés en BakeWise y quiere ser notificado cuando se lance oficialmente. + +============================================== +Recibido: {timestamp} +""" + +PRELAUNCH_EMAIL_TEMPLATE_HTML = """ + + + + + + + +
+
+

Nueva Suscripción Pre-Lanzamiento

+
+
+
🚀
+

Un nuevo usuario quiere ser notificado del lanzamiento de BakeWise

+ + + +

Añade este email a la lista de espera para el lanzamiento oficial.

+
+ +
+ + +""" + + +# ================================================================ +# HELPER FUNCTIONS +# ================================================================ + +def get_type_label(type_code: str) -> str: + """Get human-readable label for contact type""" + labels = { + "general": "Consulta General", + "technical": "Soporte Técnico", + "sales": "Ventas", + "feedback": "Feedback" + } + return labels.get(type_code, type_code) + + +def get_category_label(category: str) -> str: + """Get human-readable label for feedback category""" + labels = { + "suggestion": "Sugerencia", + "bug": "Error/Bug", + "feature": "Nueva Funcionalidad", + "praise": "Elogio", + "complaint": "Queja" + } + return labels.get(category, category) + + +def get_rating_display(rating: Optional[int]) -> str: + """Convert rating to star display""" + if rating is None: + return "N/A" + return "★" * rating + "☆" * (5 - rating) + + +# ================================================================ +# API ENDPOINTS +# ================================================================ + +@router.post("/contact", response_model=ContactFormResponse) +async def submit_contact_form(request: ContactFormRequest): + """ + Submit a contact form message. + Sends an email to contact@bakewise.ai with the form data. + """ + try: + email_service = EmailService() + + timestamp = datetime.now().strftime("%d/%m/%Y %H:%M:%S") + type_label = get_type_label(request.type) + + # Format email content + text_content = CONTACT_EMAIL_TEMPLATE_TEXT.format( + name=request.name, + email=request.email, + phone=request.phone or "No proporcionado", + bakery_name=request.bakery_name or "No proporcionado", + type=request.type, + type_label=type_label, + subject=request.subject, + message=request.message, + timestamp=timestamp + ) + + html_content = CONTACT_EMAIL_TEMPLATE_HTML.format( + name=request.name, + email=request.email, + phone=request.phone or "No proporcionado", + bakery_name=request.bakery_name or "No proporcionado", + type=request.type, + type_label=type_label, + subject=request.subject, + message=request.message, + timestamp=timestamp + ) + + # Send email to contact@bakewise.ai + success = await email_service.send_email( + to_email="contact@bakewise.ai", + subject=f"[{type_label}] {request.subject}", + text_content=text_content, + html_content=html_content, + reply_to=request.email + ) + + if success: + logger.info("Contact form submitted successfully", + from_email=request.email, + type=request.type, + subject=request.subject) + return ContactFormResponse( + success=True, + message="Mensaje enviado correctamente. Te responderemos pronto." + ) + else: + logger.error("Failed to send contact form email", + from_email=request.email) + raise HTTPException( + status_code=500, + detail="Error al enviar el mensaje. Por favor, inténtalo de nuevo." + ) + + except HTTPException: + raise + except Exception as e: + logger.error("Contact form submission error", error=str(e)) + raise HTTPException( + status_code=500, + detail="Error interno. Por favor, inténtalo más tarde." + ) + + +@router.post("/feedback", response_model=ContactFormResponse) +async def submit_feedback_form(request: FeedbackFormRequest): + """ + Submit a feedback form. + Sends an email to contact@bakewise.ai with the feedback data. + """ + try: + email_service = EmailService() + + timestamp = datetime.now().strftime("%d/%m/%Y %H:%M:%S") + category_label = get_category_label(request.category) + rating_display = get_rating_display(request.rating) + + # Format email content + text_content = FEEDBACK_EMAIL_TEMPLATE_TEXT.format( + name=request.name, + email=request.email, + category=request.category, + category_label=category_label, + rating_display=rating_display, + title=request.title, + description=request.description, + timestamp=timestamp + ) + + html_content = FEEDBACK_EMAIL_TEMPLATE_HTML.format( + name=request.name, + email=request.email, + category=request.category, + category_label=category_label, + rating_display=rating_display, + title=request.title, + description=request.description, + timestamp=timestamp + ) + + # Send email to contact@bakewise.ai + success = await email_service.send_email( + to_email="contact@bakewise.ai", + subject=f"[Feedback - {category_label}] {request.title}", + text_content=text_content, + html_content=html_content, + reply_to=request.email + ) + + if success: + logger.info("Feedback form submitted successfully", + from_email=request.email, + category=request.category, + title=request.title) + return ContactFormResponse( + success=True, + message="Gracias por tu feedback. Lo revisaremos pronto." + ) + else: + logger.error("Failed to send feedback form email", + from_email=request.email) + raise HTTPException( + status_code=500, + detail="Error al enviar el feedback. Por favor, inténtalo de nuevo." + ) + + except HTTPException: + raise + except Exception as e: + logger.error("Feedback form submission error", error=str(e)) + raise HTTPException( + status_code=500, + detail="Error interno. Por favor, inténtalo más tarde." + ) + + +@router.post("/prelaunch-subscribe", response_model=ContactFormResponse) +async def submit_prelaunch_email(request: PrelaunchEmailRequest): + """ + Submit a prelaunch email subscription. + Sends a notification email to contact@bakewise.ai. + """ + try: + email_service = EmailService() + + timestamp = datetime.now().strftime("%d/%m/%Y %H:%M:%S") + + # Format email content + text_content = PRELAUNCH_EMAIL_TEMPLATE_TEXT.format( + email=request.email, + timestamp=timestamp + ) + + html_content = PRELAUNCH_EMAIL_TEMPLATE_HTML.format( + email=request.email, + timestamp=timestamp + ) + + # Send email to contact@bakewise.ai + success = await email_service.send_email( + to_email="contact@bakewise.ai", + subject=f"[Pre-Lanzamiento] Nueva suscripción: {request.email}", + text_content=text_content, + html_content=html_content, + reply_to=request.email + ) + + if success: + logger.info("Prelaunch subscription submitted successfully", + email=request.email) + return ContactFormResponse( + success=True, + message="Te hemos apuntado a la lista de espera." + ) + else: + logger.error("Failed to send prelaunch subscription email", + email=request.email) + raise HTTPException( + status_code=500, + detail="Error al registrar tu email. Por favor, inténtalo de nuevo." + ) + + except HTTPException: + raise + except Exception as e: + logger.error("Prelaunch subscription error", error=str(e)) + raise HTTPException( + status_code=500, + detail="Error interno. Por favor, inténtalo más tarde." + ) diff --git a/services/notification/app/main.py b/services/notification/app/main.py index ecbab3b2..be9ecc93 100644 --- a/services/notification/app/main.py +++ b/services/notification/app/main.py @@ -15,6 +15,7 @@ from app.api.notification_operations import router as notification_operations_ro from app.api.analytics import router as analytics_router from app.api.audit import router as audit_router from app.api.whatsapp_webhooks import router as whatsapp_webhooks_router +from app.api.public_contact import router as public_contact_router from app.services.sse_service import SSEService from app.services.notification_orchestrator import NotificationOrchestrator from app.services.email_service import EmailService @@ -306,6 +307,7 @@ service.setup_custom_endpoints() # where {notification_id} would match literal paths like "audit-logs" service.add_router(audit_router, tags=["audit-logs"]) service.add_router(whatsapp_webhooks_router, tags=["whatsapp-webhooks"]) +service.add_router(public_contact_router, tags=["public-contact"]) service.add_router(notification_operations_router, tags=["notification-operations"]) service.add_router(analytics_router, tags=["notifications-analytics"]) service.add_router(notification_router, tags=["notifications"]) diff --git a/services/production/app/api/equipment.py b/services/production/app/api/equipment.py index 387f87ec..d2a4a6e5 100644 --- a/services/production/app/api/equipment.py +++ b/services/production/app/api/equipment.py @@ -480,12 +480,32 @@ async def trigger_failure_notifications(notification_service: any, tenant_id: UU } html_content = template.render(**template_vars) + text_content = f"Equipment failure alert: {equipment.name} - {failure_data.get('failureType', 'Unknown')}" - # Send via notification service (which will handle the actual email sending) - # This is a simplified approach - in production you'd want to get manager emails from DB - logger.info("Failure notifications triggered (template rendered)", - equipment_id=str(equipment.id), - tenant_id=str(tenant_id)) + # Send via notification service API + if notification_service: + result = await notification_service.send_email( + tenant_id=str(tenant_id), + to_email="managers@bakeryia.com", # Should be configured from DB in production + subject=f"🚨 Fallo de Equipo: {equipment.name}", + message=text_content, + html_content=html_content, + priority="high" + ) + + if result: + logger.info("Failure notifications sent via notification service", + equipment_id=str(equipment.id), + tenant_id=str(tenant_id), + notification_id=result.get('notification_id')) + else: + logger.error("Failed to send failure notifications via notification service", + equipment_id=str(equipment.id), + tenant_id=str(tenant_id)) + else: + logger.warning("Notification service not available, failure notifications not sent", + equipment_id=str(equipment.id), + tenant_id=str(tenant_id)) except Exception as e: logger.error("Error triggering failure notifications", @@ -523,11 +543,32 @@ async def trigger_repair_notifications(notification_service: any, tenant_id: UUI } html_content = template.render(**template_vars) + text_content = f"Equipment repair completed: {equipment.name} - {repair_data.get('repairDescription', 'Repair completed')}" - # Send via notification service - logger.info("Repair notifications triggered (template rendered)", - equipment_id=str(equipment.id), - tenant_id=str(tenant_id)) + # Send via notification service API + if notification_service: + result = await notification_service.send_email( + tenant_id=str(tenant_id), + to_email="managers@bakeryia.com", # Should be configured from DB in production + subject=f"✅ Equipo Reparado: {equipment.name}", + message=text_content, + html_content=html_content, + priority="normal" + ) + + if result: + logger.info("Repair notifications sent via notification service", + equipment_id=str(equipment.id), + tenant_id=str(tenant_id), + notification_id=result.get('notification_id')) + else: + logger.error("Failed to send repair notifications via notification service", + equipment_id=str(equipment.id), + tenant_id=str(tenant_id)) + else: + logger.warning("Notification service not available, repair notifications not sent", + equipment_id=str(equipment.id), + tenant_id=str(tenant_id)) except Exception as e: logger.error("Error triggering repair notifications", @@ -565,14 +606,35 @@ async def send_support_contact_notification(notification_service: any, tenant_id } html_content = template.render(**template_vars) + text_content = f"Equipment failure alert: {equipment.name} - {failure_data.get('failureType', 'Unknown')}" - # TODO: Actually send email via notification service - # For now, just log that we would send to the support email - logger.info("Support contact notification prepared (would send to support)", - equipment_id=str(equipment.id), - tenant_id=str(tenant_id), - support_email=support_email, - subject=f"🚨 URGENTE: Fallo de Equipo - {equipment.name}") + # Send via notification service API + if notification_service and support_email: + result = await notification_service.send_email( + tenant_id=str(tenant_id), + to_email=support_email, + subject=f"🚨 URGENTE: Fallo de Equipo - {equipment.name}", + message=text_content, + html_content=html_content, + priority="high" + ) + + if result: + logger.info("Support contact notification sent via notification service", + equipment_id=str(equipment.id), + tenant_id=str(tenant_id), + support_email=support_email, + notification_id=result.get('notification_id')) + else: + logger.error("Failed to send support contact notification via notification service", + equipment_id=str(equipment.id), + tenant_id=str(tenant_id), + support_email=support_email) + else: + logger.warning("Notification service not available or no support email provided", + equipment_id=str(equipment.id), + tenant_id=str(tenant_id), + support_email=support_email) except Exception as e: logger.error("Error sending support contact notification", diff --git a/services/suppliers/app/consumers/alert_event_consumer.py b/services/suppliers/app/consumers/alert_event_consumer.py index 04c8921c..a40798cd 100644 --- a/services/suppliers/app/consumers/alert_event_consumer.py +++ b/services/suppliers/app/consumers/alert_event_consumer.py @@ -22,9 +22,10 @@ class AlertEventConsumer: Handles email and Slack notifications for critical alerts """ - def __init__(self, db_session: AsyncSession): + def __init__(self, db_session: AsyncSession, notification_client: Optional[any] = None): self.db_session = db_session self.notification_config = self._load_notification_config() + self.notification_client = notification_client def _load_notification_config(self) -> Dict[str, Any]: """ @@ -451,7 +452,7 @@ class AlertEventConsumer: data: Dict[str, Any] ) -> bool: """ - Send email notification + Send email notification using notification service API Args: tenant_id: Tenant ID @@ -466,42 +467,77 @@ class AlertEventConsumer: logger.debug("Email notifications disabled") return False - import smtplib - from email.mime.text import MIMEText - from email.mime.multipart import MIMEMultipart + # Use notification service client if available + if self.notification_client: + # Build email content + subject = self._format_email_subject(notification_type, data) + body = self._format_email_body(notification_type, data) - # Build email content - subject = self._format_email_subject(notification_type, data) - body = self._format_email_body(notification_type, data) + # Send via notification service API + result = await self.notification_client.send_email( + tenant_id=tenant_id, + to_email=", ".join(self.notification_config['email']['recipients']), + subject=subject, + message="Supplier alert notification", # Plain text fallback + html_content=body, + priority="high" if data.get('severity') == 'critical' else "normal" + ) - # Create message - msg = MIMEMultipart('alternative') - msg['Subject'] = subject - msg['From'] = self.notification_config['email']['from_address'] - msg['To'] = ', '.join(self.notification_config['email']['recipients']) + if result: + logger.info( + "Email notification sent via notification service", + tenant_id=tenant_id, + notification_type=notification_type, + recipients=len(self.notification_config['email']['recipients']) + ) + return True + else: + logger.error( + "Notification service failed to send email", + tenant_id=tenant_id, + notification_type=notification_type + ) + return False + else: + # Fallback to direct SMTP for backward compatibility + logger.warning("Notification client not available, falling back to direct SMTP") + + import smtplib + from email.mime.text import MIMEText + from email.mime.multipart import MIMEMultipart - # Attach HTML body - html_part = MIMEText(body, 'html') - msg.attach(html_part) + # Build email content + subject = self._format_email_subject(notification_type, data) + body = self._format_email_body(notification_type, data) - # Send email - smtp_config = self.notification_config['email'] - with smtplib.SMTP(smtp_config['smtp_host'], smtp_config['smtp_port']) as server: - if smtp_config['use_tls']: - server.starttls() + # Create message + msg = MIMEMultipart('alternative') + msg['Subject'] = subject + msg['From'] = self.notification_config['email']['from_address'] + msg['To'] = ', '.join(self.notification_config['email']['recipients']) - if smtp_config['smtp_username'] and smtp_config['smtp_password']: - server.login(smtp_config['smtp_username'], smtp_config['smtp_password']) + # Attach HTML body + html_part = MIMEText(body, 'html') + msg.attach(html_part) - server.send_message(msg) + # Send email + smtp_config = self.notification_config['email'] + with smtplib.SMTP(smtp_config['smtp_host'], smtp_config['smtp_port']) as server: + if smtp_config['use_tls']: + server.starttls() - logger.info( - "Email notification sent", - tenant_id=tenant_id, - notification_type=notification_type, - recipients=len(self.notification_config['email']['recipients']) - ) - return True + if smtp_config['smtp_username'] and smtp_config['smtp_password']: + server.login(smtp_config['smtp_username'], smtp_config['smtp_password']) + + server.send_message(msg) + + logger.info( + "Email notification sent via direct SMTP (fallback)", + tenant_id=tenant_id, + notification_type=notification_type, + recipients=len(self.notification_config['email']['recipients']) + ) + return True except Exception as e: logger.error( @@ -785,6 +821,6 @@ class AlertEventConsumer: # Factory function for creating consumer instance -def create_alert_event_consumer(db_session: AsyncSession) -> AlertEventConsumer: +def create_alert_event_consumer(db_session: AsyncSession, notification_client: Optional[any] = None) -> AlertEventConsumer: """Create alert event consumer instance""" - return AlertEventConsumer(db_session) + return AlertEventConsumer(db_session, notification_client)