Skip to content

Commit 0021a3b

Browse files
committed
feat: add zip code field for Mexico checkout and implement cross-platform latency resilience experiments
1 parent 75a849f commit 0021a3b

File tree

11 files changed

+285
-4
lines changed

11 files changed

+285
-4
lines changed

backend/constants.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ class CountryCode(str, Enum):
5454
"currency": "MXN",
5555
"currency_symbol": "$",
5656
"required_fields": ["colonia"],
57-
"optional_fields": ["propina"],
57+
"optional_fields": ["propina", "zip_code"],
5858
"tax_rate": 0.0,
5959
"languages": ["es"]
6060
},

backend/main.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,8 @@ async def checkout(
231231
# Add country-specific fields
232232
if request.country_code == CountryCode.MX:
233233
order_data["customer_info"]["colonia"] = request.colonia
234+
if request.zip_code:
235+
order_data["customer_info"]["zip_code"] = request.zip_code
234236
if request.propina:
235237
order_data["customer_info"]["propina"] = request.propina
236238
elif request.country_code == CountryCode.US:

frontend-mobile/.detoxrc.js

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/** @type {Detox.DetoxConfig} */
2+
module.exports = {
3+
testRunner: {
4+
args: {
5+
'$0': 'jest',
6+
config: 'e2e/jest.config.js'
7+
},
8+
jest: {
9+
setupTimeout: 120000
10+
}
11+
},
12+
apps: {
13+
'ios.sim.release': {
14+
type: 'ios.simulator',
15+
build: 'pnpm build:ios:simulator',
16+
binaryPath: 'ios/build/Build/Products/Release-iphonesimulator/OmniPizza.app'
17+
},
18+
'android.emu.release': {
19+
type: 'android.emulator',
20+
build: 'pnpm build:android:test',
21+
binaryPath: 'android/app/build/outputs/apk/release/omnipizza-release.apk',
22+
testBinaryPath: 'android/app/build/outputs/apk/androidTest/debug/omnipizza-debug-androidTest.apk'
23+
}
24+
},
25+
devices: {
26+
simulator: {
27+
type: 'ios.simulator',
28+
device: {
29+
type: 'iPhone 15'
30+
}
31+
},
32+
emulator: {
33+
type: 'android.emulator',
34+
device: {
35+
avdName: 'Pixel_4_API_31'
36+
}
37+
}
38+
},
39+
configurations: {
40+
'ios.sim.release': {
41+
device: 'simulator',
42+
app: 'ios.sim.release'
43+
},
44+
'android.emu.release': {
45+
device: 'emulator',
46+
app: 'android.emu.release'
47+
}
48+
}
49+
};
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
const fs = require('fs');
2+
const path = require('path');
3+
const { performance } = require('perf_hooks');
4+
5+
describe('Experimento IEEE: Resiliencia de UI bajo Latencia Estocástica', () => {
6+
const markets = ['US', 'CH', 'JP', 'MX'];
7+
const profiles = ['standard_user', 'performance_glitch_user'];
8+
9+
// Función síncrona para asegurar escritura thread-safe durante la inyección en parallel
10+
const logIEEEData = (experiment, profile, market, platform, latencyMs) => {
11+
const timestamp = new Date().toISOString();
12+
const csvLine = `${timestamp},${experiment},${profile},${market},${platform},${latencyMs.toFixed(2)}\n`;
13+
14+
// Escribe métricas en la carpeta principal compartida con cypress
15+
const resultsDir = path.join(__dirname, '..', '..', '..', 'frontend', 'cypress', 'results');
16+
const filePath = path.join(resultsDir, 'ieee_latency_data.csv');
17+
18+
if (!fs.existsSync(resultsDir)) {
19+
fs.mkdirSync(resultsDir, { recursive: true });
20+
}
21+
22+
if (!fs.existsSync(filePath)) {
23+
fs.writeFileSync(filePath, 'timestamp,experiment,profile,market,platform,latency_ms\n');
24+
}
25+
26+
// sync flag asegura write operations en caso que estemos integrando CI paralelismo multihilo
27+
fs.appendFileSync(filePath, csvLine);
28+
};
29+
30+
profiles.forEach((profile) => {
31+
describe(`Evaluando perfil de inyección: ${profile}`, () => {
32+
33+
markets.forEach((market) => {
34+
it(`Mide la latencia de renderizado para el mercado ${market}`, async () => {
35+
36+
await device.launchApp({
37+
newInstance: true,
38+
launchArgs: { detoxCountryCode: market }
39+
});
40+
41+
// 1. API-Driven State Hydration (Login automatizado)
42+
await waitFor(element(by.id('screen-login'))).toBeVisible().withTimeout(15000);
43+
44+
await element(by.id('input-username')).typeText(profile);
45+
await element(by.id('input-password')).typeText('pizza123'); // Preset estándar para dev/qa
46+
47+
// Prevenir keyboard occlusion si la pantalla fue renderizada pequeña
48+
// Para asegurar consistencia de touch actions en iOS/Android
49+
await element(by.id('btn-login')).tap();
50+
51+
// Espera a que estemos dentro de la app (Ej: visualización del View container de catálogo o bottom nav)
52+
await waitFor(element(by.id('view-bottom-nav'))).toBeVisible().withTimeout(15000);
53+
54+
// 2. Medición precisa de FID proxy en el Checkout Container
55+
const startTime = performance.now();
56+
57+
await element(by.id('nav-checkout')).tap();
58+
59+
// Espera a que complete el mount cycle del market checkout respectivo
60+
await waitFor(element(by.id('screen-checkout'))).toBeVisible().withTimeout(15000);
61+
62+
const endTime = performance.now();
63+
const renderLatency = endTime - startTime;
64+
65+
console.log(`[DATA_POINT] Perfil: ${profile} | Mercado: ${market} | Latencia: ${renderLatency.toFixed(2)} ms`);
66+
67+
// 3. Escribir resultados adjuntando platform: 'mobile'
68+
logIEEEData('Fuzzy_Wait_States', profile, market, 'mobile', renderLatency);
69+
});
70+
});
71+
});
72+
});
73+
});

frontend-mobile/src/api/client.ts

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,28 @@
11
import axios from "axios";
22
import { useAppStore } from "../store/useAppStore";
3+
import { NativeModules } from "react-native";
4+
5+
// Fallback logic for Detox launch args
6+
let detoxCountryCode: string | null = null;
7+
try {
8+
// Safe accessor for React Native launch arguments
9+
if (NativeModules.LaunchArguments) {
10+
const args = typeof NativeModules.LaunchArguments.getArguments === 'function'
11+
? NativeModules.LaunchArguments.getArguments()
12+
: NativeModules.LaunchArguments;
13+
if (args && args.detoxCountryCode) {
14+
detoxCountryCode = args.detoxCountryCode;
15+
}
16+
} else {
17+
// Para versiones modernas de React Native Utilities
18+
const RNLaunchArgs = require('react-native/Libraries/Utilities/LaunchArguments');
19+
if (RNLaunchArgs && RNLaunchArgs.LaunchArguments && RNLaunchArgs.LaunchArguments.detoxCountryCode) {
20+
detoxCountryCode = RNLaunchArgs.LaunchArguments.detoxCountryCode;
21+
}
22+
}
23+
} catch (e) {
24+
// Silent fallback
25+
}
326

427
const API_ORIGIN = "https://omnipizza-backend.onrender.com";
528

@@ -12,9 +35,15 @@ apiClient.interceptors.request.use((config) => {
1235
const { country, language, token } = useAppStore.getState();
1336

1437
config.headers = config.headers ?? {};
15-
config.headers["X-Country-Code"] = country || "MX";
16-
config.headers["X-Language"] = language || "en";
17-
if (token) config.headers["Authorization"] = `Bearer ${token}`;
38+
39+
// Retrocompatibilidad: Priorizar config.headers explícito explícito (por ejemplo en tests individuales),
40+
// y usar el global inyectado por Detox si se especificó, sino fallback en store.
41+
config.headers["X-Country-Code"] = config.headers["X-Country-Code"] || detoxCountryCode || country || "MX";
42+
config.headers["X-Language"] = config.headers["X-Language"] || language || "en";
43+
44+
if (token && !config.headers["Authorization"]) {
45+
config.headers["Authorization"] = `Bearer ${token}`;
46+
}
1847

1948
return config;
2049
});

frontend-mobile/src/features/checkout/useCases/buildCheckoutPayload.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ export function buildCheckoutPayload(input: {
3737

3838
if (country === "MX") {
3939
payload.colonia = form.colonia.trim();
40+
if (form.zip_code) payload.zip_code = form.zip_code.trim();
4041
if (form.propina) payload.propina = Number(form.propina);
4142
} else if (country === "US") {
4243
payload.zip_code = form.zip_code.trim();

frontend-mobile/src/screens/CheckoutScreen.tsx

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,23 @@ export default function CheckoutScreen({ navigation }: any) {
246246
/>
247247
</View>
248248
)}
249+
{country === "MX" && (
250+
<View style={{ marginTop: 12 }} accessibilityLabel="view-field-zipcode-mx">
251+
<Text style={styles.cardFieldLabel} accessibilityLabel="label-zipcode-mx">{t("zipCode")}</Text>
252+
<TextInput
253+
style={styles.cardInput}
254+
placeholder="06600"
255+
placeholderTextColor="#555"
256+
keyboardType="number-pad"
257+
value={form.zip_code}
258+
onChangeText={(v) =>
259+
setForm((p) => ({ ...p, zip_code: v.replace(/[^0-9]/g, "") }))
260+
}
261+
accessibilityLabel="input-zipcode-mx"
262+
testID="input-zipcode-mx"
263+
/>
264+
</View>
265+
)}
249266
{country === "US" && (
250267
<View style={{ marginTop: 12 }} accessibilityLabel="view-field-zipcode">
251268
<Text style={styles.cardFieldLabel} accessibilityLabel="label-zipcode">{t("zipCode")}</Text>

frontend/cypress.config.js

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
const { defineConfig } = require("cypress");
2+
const fs = require('fs');
3+
const path = require('path');
24

35
module.exports = defineConfig({
46
video: false,
@@ -12,4 +14,36 @@ module.exports = defineConfig({
1214
supportFile: "cypress/support/component.js",
1315
indexHtmlFile: "cypress/support/component-index.html",
1416
},
17+
e2e: {
18+
setupNodeEvents(on, config) {
19+
// Register the task in Node events
20+
on('task', {
21+
logIEEEData({ experiment, profile, market, platform, latencyMs }) {
22+
// Format the CSV line
23+
const timestamp = new Date().toISOString();
24+
const csvLine = `${timestamp},${experiment},${profile},${market},${platform},${latencyMs.toFixed(2)}\n`;
25+
26+
// Define the output path
27+
const resultsDir = path.join(__dirname, 'cypress', 'results');
28+
const filePath = path.join(resultsDir, 'ieee_latency_data.csv');
29+
30+
// Create the directory if it doesn't exist
31+
if (!fs.existsSync(resultsDir)) {
32+
fs.mkdirSync(resultsDir, { recursive: true });
33+
}
34+
35+
// Create the file and headers if it's the first execution
36+
if (!fs.existsSync(filePath)) {
37+
fs.writeFileSync(filePath, 'timestamp,experiment,profile,market,platform,latency_ms\n');
38+
}
39+
40+
// Append the new metric to the file
41+
fs.appendFileSync(filePath, csvLine);
42+
43+
// Cypress requires tasks to return null if there is no return value
44+
return null;
45+
},
46+
});
47+
},
48+
},
1549
});
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
describe('Experimento IEEE: Resiliencia de UI bajo Latencia Estocástica', () => {
2+
const markets = ['US', 'CH', 'JP', 'MX'];
3+
const profiles = ['standard_user', 'performance_glitch_user'];
4+
5+
profiles.forEach((profile) => {
6+
context(`Evaluando perfil de inyección: ${profile}`, () => {
7+
8+
markets.forEach((market) => {
9+
it(`Mide la latencia de renderizado (FID proxy) para el mercado ${market}`, () => {
10+
11+
// 1. Intercept requests to inject the Dynamic Configuration Model
12+
cy.intercept('**/api/*', (req) => {
13+
req.headers['X-Country-Code'] = market;
14+
}).as(`apiCalls_${market}`);
15+
16+
// 2. Hydration of API-Driven state (Login)
17+
cy.visit('/login');
18+
cy.get('[data-test="username"]').type(profile);
19+
cy.get('[data-test="password"]').type('omnipizza_secret');
20+
cy.get('[data-test="login-button"]').click();
21+
22+
// 3. Measuring Recovery/Rendering Time
23+
cy.window().then((win) => {
24+
const startTime = win.performance.now();
25+
26+
// Navigate to the checkout, which is the area of greatest variability
27+
cy.get('[data-test="checkout-link"]').click();
28+
29+
// Wait for the main market form to be fully interactive
30+
cy.get('[data-test="checkout-form-container"]', { timeout: 15000 })
31+
.should('be.visible')
32+
.then(() => {
33+
const endTime = win.performance.now();
34+
const renderLatency = endTime - startTime;
35+
36+
// 4. Output data for statistical analysis of the paper
37+
cy.log(`[DATA_POINT] Perfil: ${profile} | Mercado: ${market} | Latencia: ${renderLatency.toFixed(2)} ms`);
38+
39+
// Optional: Write this to a CSV file using cy.writeFile or cy.task
40+
cy.task('logIEEEData', {
41+
experiment: 'Fuzzy_Wait_States',
42+
profile: profile,
43+
market: market,
44+
platform: 'web',
45+
latencyMs: renderLatency
46+
});
47+
});
48+
});
49+
});
50+
});
51+
});
52+
});
53+
});

frontend/src/features/checkout/useCases/buildCheckoutPayload.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export function buildCheckoutPayload({ countryCode, items, form }) {
1414

1515
if (countryCode === "MX") {
1616
payload.colonia = form.colonia;
17+
if (form.zip_code) payload.zip_code = form.zip_code;
1718
if (form.propina) payload.propina = parseFloat(form.propina);
1819
} else if (countryCode === "US") {
1920
payload.zip_code = form.zip_code;

0 commit comments

Comments
 (0)