feat(bold): integrar pasarela Bold API con QR Bre-B y sondeo en vivo para separacion y contraentrega
This commit is contained in:
parent
61803fe15a
commit
182b2600c0
BIN
assets_generados/QRBrebBold.jpeg
Normal file
BIN
assets_generados/QRBrebBold.jpeg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 123 KiB |
@ -346,5 +346,76 @@ object ApiClient {
|
||||
Result.failure(e)
|
||||
}
|
||||
}
|
||||
|
||||
data class BoldPaymentData(
|
||||
val paymentLink: String,
|
||||
val checkoutUrl: String,
|
||||
val qrImageUrl: String,
|
||||
val amount: Double
|
||||
)
|
||||
|
||||
/**
|
||||
* Genera un link y Código QR Bre-B para pagar el anticipo o saldo en Bold
|
||||
*/
|
||||
suspend fun createBoldPayment(orderId: String, amount: Double = 10000.0, type: String = "deposit"): Result<BoldPaymentData> =
|
||||
withContext(Dispatchers.IO) {
|
||||
try {
|
||||
val payload = mapOf(
|
||||
"orderId" to orderId,
|
||||
"amount" to amount,
|
||||
"type" to type
|
||||
)
|
||||
val jsonPayload = gson.toJson(payload)
|
||||
val mediaType = "application/json; charset=utf-8".toMediaTypeOrNull()
|
||||
val requestBody = jsonPayload.toRequestBody(mediaType)
|
||||
|
||||
val request = Request.Builder()
|
||||
.url("https://dripzone.almeira.com.co/api/bold/create-payment")
|
||||
.post(requestBody)
|
||||
.header("Content-Type", "application/json")
|
||||
.header("Accept", "application/json")
|
||||
.build()
|
||||
|
||||
val response = client.newCall(request).execute()
|
||||
val responseBody = response.body?.string() ?: ""
|
||||
val json = gson.fromJson(responseBody, com.google.gson.JsonObject::class.java)
|
||||
|
||||
if (response.isSuccessful && json.get("success")?.asBoolean == true) {
|
||||
val link = json.get("paymentLink")?.asString ?: ""
|
||||
val url = json.get("checkoutUrl")?.asString ?: ""
|
||||
val qr = json.get("qrImageUrl")?.asString ?: ""
|
||||
val amt = json.get("amount")?.asDouble ?: amount
|
||||
Result.success(BoldPaymentData(link, url, qr, amt))
|
||||
} else {
|
||||
val err = json.get("error")?.asString ?: "Error al generar cobro Bold"
|
||||
Result.failure(Exception(err))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error createBoldPayment: ${e.message}")
|
||||
Result.failure(e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Consulta si la transacción ya fue pagada en Bold
|
||||
*/
|
||||
suspend fun checkBoldPaymentStatus(linkId: String, orderId: String): Result<Boolean> =
|
||||
withContext(Dispatchers.IO) {
|
||||
try {
|
||||
val request = Request.Builder()
|
||||
.url("https://dripzone.almeira.com.co/api/bold/status?linkId=$linkId&orderId=$orderId")
|
||||
.get()
|
||||
.header("Accept", "application/json")
|
||||
.build()
|
||||
|
||||
val response = client.newCall(request).execute()
|
||||
val responseBody = response.body?.string() ?: ""
|
||||
val json = gson.fromJson(responseBody, com.google.gson.JsonObject::class.java)
|
||||
val isPaid = json?.get("isPaid")?.asBoolean ?: false
|
||||
Result.success(isPaid)
|
||||
} catch (e: Exception) {
|
||||
Result.failure(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -15,7 +15,11 @@ import co.dripzone.app.model.CartManager
|
||||
import co.dripzone.app.model.City
|
||||
import co.dripzone.app.model.UserManager
|
||||
import co.dripzone.app.security.SecurityManager
|
||||
import com.bumptech.glide.Glide
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
import java.text.NumberFormat
|
||||
import java.util.Locale
|
||||
@ -33,6 +37,7 @@ class OrderVerificationBottomSheet : BottomSheetDialogFragment() {
|
||||
private var customerPhone: String = ""
|
||||
private var customerAddress: String = ""
|
||||
private var createdOrderId: String = ""
|
||||
private var pollingJob: Job? = null
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
@ -283,12 +288,56 @@ class OrderVerificationBottomSheet : BottomSheetDialogFragment() {
|
||||
binding.tvSuccessOrderId.text = "Orden #$orderId"
|
||||
|
||||
if (requiresDeposit) {
|
||||
binding.tvSuccessMessage.text = "¡Tu número de WhatsApp (+57 $customerPhone) fue validado y tu pedido fue registrado en la plataforma!\n\n⚠️ REQUISITO PARA DESPACHO:\nPara poder programar el despacho de tu paquete, realiza el pago de la separación de $10.000 COP a través de la pasarela oficial Bold. Cancelarás el saldo restante (${format.format(balanceDue)} COP) en efectivo al recibir."
|
||||
binding.tvSuccessMessage.text = "¡Tu número de WhatsApp (+57 $customerPhone) fue validado y tu pedido fue registrado en la plataforma!\n\n⚠️ REQUISITO PARA DESPACHO:\nRealiza el pago de la separación de $10.000 COP para coordinar y programar el despacho de tu paquete. Cancelarás el saldo restante (${format.format(balanceDue)} COP) en efectivo al recibir."
|
||||
binding.layoutBoldQrContainer.visibility = View.VISIBLE
|
||||
binding.pbBoldQrLoading.visibility = View.VISIBLE
|
||||
binding.ivBoldQrCode.setImageDrawable(null)
|
||||
binding.btnPaySeparationBold.visibility = View.VISIBLE
|
||||
|
||||
// Generar dinámicamente Link de Pago y QR Bre-B en Bold
|
||||
lifecycleScope.launch {
|
||||
val boldResult = ApiClient.createBoldPayment(orderId, 10000.0, "deposit")
|
||||
binding.pbBoldQrLoading.visibility = View.GONE
|
||||
|
||||
boldResult.onSuccess { boldData ->
|
||||
// Renderizar QR Code Bre-B
|
||||
if (boldData.qrImageUrl.isNotBlank()) {
|
||||
Glide.with(this@OrderVerificationBottomSheet)
|
||||
.load(boldData.qrImageUrl)
|
||||
.into(binding.ivBoldQrCode)
|
||||
}
|
||||
|
||||
// Configurar botón para abrir pasarela de pago Bold
|
||||
val checkoutUrl = if (boldData.checkoutUrl.isNotBlank()) boldData.checkoutUrl else "https://bold.co/p/carlos-lasso"
|
||||
binding.btnPaySeparationBold.setOnClickListener {
|
||||
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(checkoutUrl))
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
// Iniciar sondeo en vivo para confirmar pago en tiempo real
|
||||
pollingJob?.cancel()
|
||||
pollingJob = lifecycleScope.launch {
|
||||
while (isActive) {
|
||||
delay(4000)
|
||||
val statusRes = ApiClient.checkBoldPaymentStatus(boldData.paymentLink, orderId)
|
||||
if (statusRes.isSuccess && statusRes.getOrNull() == true) {
|
||||
binding.layoutBoldQrContainer.visibility = View.GONE
|
||||
binding.btnPaySeparationBold.visibility = View.GONE
|
||||
binding.layoutBoldPaidSuccess.visibility = View.VISIBLE
|
||||
binding.tvSuccessMessage.text = "¡Anticipo de $10.000 COP recibido y confirmado por Bold! Tu pedido ha sido programado para despacho inmediato."
|
||||
Toast.makeText(requireContext(), "🎉 ¡Anticipo verificado en tiempo real con Bold!", Toast.LENGTH_LONG).show()
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}.onFailure {
|
||||
binding.btnPaySeparationBold.setOnClickListener {
|
||||
val intent = Intent(Intent.ACTION_VIEW, Uri.parse("https://bold.co/p/carlos-lasso"))
|
||||
startActivity(intent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
binding.btnOpenWhatsAppChat.text = "Notificar Pago a Carlos en WhatsApp"
|
||||
binding.btnOpenWhatsAppChat.setOnClickListener {
|
||||
val message = "¡Hola Carlos! Acabo de registrar mi pedido en DripZone con la orden *$createdOrderId*. Voy a cancelar la separación de $10.000 COP para coordinar el despacho a ${selectedCity.name}. Saldo al recibir: ${format.format(balanceDue)} COP."
|
||||
@ -297,6 +346,8 @@ class OrderVerificationBottomSheet : BottomSheetDialogFragment() {
|
||||
}
|
||||
} else {
|
||||
binding.tvSuccessMessage.text = "¡Tu número de WhatsApp (+57 $customerPhone) fue validado y tu pedido fue registrado en la plataforma!\n\n⭐ BENEFICIO CLIENTE FRECUENTE:\nComo ya realizaste compras satisfactoriamente, estás exento de separación y pagarás el 100% (${format.format(total)} COP) contraentrega al recibir tu paquete."
|
||||
binding.layoutBoldQrContainer.visibility = View.GONE
|
||||
binding.layoutBoldPaidSuccess.visibility = View.GONE
|
||||
binding.btnPaySeparationBold.visibility = View.GONE
|
||||
binding.btnOpenWhatsAppChat.text = "Coordinar Entrega por WhatsApp"
|
||||
binding.btnOpenWhatsAppChat.setOnClickListener {
|
||||
@ -314,6 +365,8 @@ class OrderVerificationBottomSheet : BottomSheetDialogFragment() {
|
||||
|
||||
override fun onDestroyView() {
|
||||
super.onDestroyView()
|
||||
pollingJob?.cancel()
|
||||
pollingJob = null
|
||||
_binding = null
|
||||
}
|
||||
|
||||
|
||||
@ -323,14 +323,111 @@
|
||||
android:textColor="@color/text_secondary"
|
||||
android:textSize="12sp" />
|
||||
|
||||
<!-- Dynamic Bold QR Bre-B Container -->
|
||||
<LinearLayout
|
||||
android:id="@+id/layoutBoldQrContainer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="14dp"
|
||||
android:background="@drawable/bg_product_card"
|
||||
android:orientation="vertical"
|
||||
android:padding="14dp"
|
||||
android:visibility="gone">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:fontFamily="sans-serif-black"
|
||||
android:text="⚡ Anticipo de Separación: $10.000 COP"
|
||||
android:textColor="@color/drip_purple_glow"
|
||||
android:textSize="13sp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvBoldQrInstruction"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:gravity="center"
|
||||
android:text="Escanea este Código QR Bre-B desde Bancolombia, Nequi, Daviplata o cualquier app bancaria:"
|
||||
android:textColor="@color/text_secondary"
|
||||
android:textSize="11sp" />
|
||||
|
||||
<!-- White Card Container for QR -->
|
||||
<FrameLayout
|
||||
android:layout_width="200dp"
|
||||
android:layout_height="200dp"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:layout_marginTop="10dp"
|
||||
android:background="@drawable/bg_chip_size"
|
||||
android:backgroundTint="#FFFFFF"
|
||||
android:padding="8dp">
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/pbBoldQrLoading"
|
||||
android:layout_width="36dp"
|
||||
android:layout_height="36dp"
|
||||
android:layout_gravity="center"
|
||||
android:indeterminateTint="@color/drip_purple" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/ivBoldQrCode"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:scaleType="fitCenter" />
|
||||
</FrameLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvBoldRealtimeStatus"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="⏳ Esperando confirmación de pago en vivo..."
|
||||
android:textColor="@color/text_muted"
|
||||
android:textSize="10sp" />
|
||||
</LinearLayout>
|
||||
|
||||
<!-- Card de Éxito cuando el Anticipo es Pagado -->
|
||||
<LinearLayout
|
||||
android:id="@+id/layoutBoldPaidSuccess"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="14dp"
|
||||
android:background="@drawable/bg_product_card"
|
||||
android:backgroundTint="#052E16"
|
||||
android:orientation="vertical"
|
||||
android:padding="14dp"
|
||||
android:visibility="gone">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:fontFamily="sans-serif-black"
|
||||
android:text="🎉 ¡ANTICIPO CONFIRMADO CON ÉXITO!"
|
||||
android:textColor="#34D399"
|
||||
android:textSize="14sp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvBoldPaidSummary"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="6dp"
|
||||
android:gravity="center"
|
||||
android:text="Tu pago de $10.000 COP fue verificado por Bold. Tu pedido fue autorizado para despacho inmediato."
|
||||
android:textColor="#A7F3D0"
|
||||
android:textSize="11sp" />
|
||||
</LinearLayout>
|
||||
|
||||
<!-- Separation Payment Button (Pasarela Bold) -->
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btnPaySeparationBold"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="52dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginTop="14dp"
|
||||
android:filterTouchesWhenObscured="true"
|
||||
android:text="Pagar Separación con Bold ($10.000 COP)"
|
||||
android:text="Pagar con Bold (PSE / Tarjeta / Nequi)"
|
||||
android:textAllCaps="false"
|
||||
android:textColor="#FFFFFF"
|
||||
android:textSize="14sp"
|
||||
|
||||
@ -19,6 +19,11 @@ services:
|
||||
- EVOLUTION_API_KEY=EE49029EEB85-4568-8F81-C9FDFC6E2943
|
||||
- EVOLUTION_INSTANCE_NAME=AutomataAlmeira
|
||||
- GEMINI_API_KEY=AIzaSyC7JMkREYxl4qoEUGzd4hzbJ6RFg_YMxx0
|
||||
- BOLD_ENVIRONMENT=sandbox
|
||||
- BOLD_SANDBOX_IDENTITY_KEY=GatyXH-WVfX1pv17z8GAoNekbqps8Mdxdvj0yRg4BSk
|
||||
- BOLD_SANDBOX_SECRET_KEY=BAFlCPYr6UFtQCvObJxTIA
|
||||
- BOLD_PROD_IDENTITY_KEY=xPlwpPJaT7J604ZY5upk0s9ZZu54p4UGQqrZnU0OzU0
|
||||
- BOLD_PROD_SECRET_KEY=Yyzm4vZjWe_N_I_5c1g7Dw
|
||||
volumes:
|
||||
- ./public/DripZone.apk:/app/public/DripZone.apk:ro
|
||||
networks:
|
||||
|
||||
61
dripzone-app/src/app/api/bold/create-payment/route.ts
Normal file
61
dripzone-app/src/app/api/bold/create-payment/route.ts
Normal file
@ -0,0 +1,61 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { createBoldPayment } from '@/lib/bold-service';
|
||||
import { dataService } from '@/lib/data-service';
|
||||
import { updateOrderInAppwrite, getOrdersFromAppwrite } from '@/lib/server-appwrite';
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
const { orderId, amount = 10000, type = 'deposit', payerEmail, redirectionUrl } = body;
|
||||
|
||||
if (!orderId) {
|
||||
return NextResponse.json({ success: false, error: 'El ID de la orden es obligatorio' }, { status: 400 });
|
||||
}
|
||||
|
||||
const description = type === 'deposit'
|
||||
? `Anticipo Separacion DripZone #${orderId}`
|
||||
: `Saldo Contraentrega DripZone #${orderId}`;
|
||||
|
||||
const payment = await createBoldPayment({
|
||||
orderId,
|
||||
amount: Number(amount),
|
||||
description,
|
||||
payerEmail,
|
||||
redirectionUrl: redirectionUrl || `https://dripzone.almeira.com.co/checkout?orderId=${orderId}`,
|
||||
type,
|
||||
});
|
||||
|
||||
if (!payment.success) {
|
||||
return NextResponse.json({ success: false, error: payment.error }, { status: 500 });
|
||||
}
|
||||
|
||||
// Registrar los datos del link de pago en la orden
|
||||
const updatePayload: Record<string, any> = {};
|
||||
if (type === 'deposit') {
|
||||
updatePayload.depositPaymentLinkId = payment.paymentLink;
|
||||
updatePayload.depositPaymentUrl = payment.checkoutUrl;
|
||||
} else {
|
||||
updatePayload.deliveryPaymentLinkId = payment.paymentLink;
|
||||
updatePayload.deliveryPaymentUrl = payment.checkoutUrl;
|
||||
}
|
||||
|
||||
try {
|
||||
dataService.updateOrder(orderId, updatePayload);
|
||||
await updateOrderInAppwrite(orderId, updatePayload);
|
||||
} catch (dbErr) {
|
||||
console.warn('Advertencia al guardar link de pago en orden:', dbErr);
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
paymentLink: payment.paymentLink,
|
||||
checkoutUrl: payment.checkoutUrl,
|
||||
qrImageUrl: payment.qrImageUrl,
|
||||
orderId,
|
||||
amount: payment.amount,
|
||||
environment: payment.environment,
|
||||
});
|
||||
} catch (error: any) {
|
||||
return NextResponse.json({ success: false, error: error.message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
92
dripzone-app/src/app/api/bold/status/route.ts
Normal file
92
dripzone-app/src/app/api/bold/status/route.ts
Normal file
@ -0,0 +1,92 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { getBoldPaymentStatus } from '@/lib/bold-service';
|
||||
import { dataService } from '@/lib/data-service';
|
||||
import { updateOrderInAppwrite, getOrdersFromAppwrite } from '@/lib/server-appwrite';
|
||||
import { sendWhatsAppMessage } from '@/lib/evolution-api';
|
||||
import { formatCOP } from '@/lib/pricing-engine';
|
||||
|
||||
export async function GET(request: Request) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const linkId = searchParams.get('linkId');
|
||||
const orderId = searchParams.get('orderId');
|
||||
|
||||
if (!linkId && !orderId) {
|
||||
return NextResponse.json({ success: false, error: 'linkId o orderId requerido' }, { status: 400 });
|
||||
}
|
||||
|
||||
// Buscar la orden
|
||||
let order = orderId ? dataService.getOrderById(orderId) : null;
|
||||
if (!order && linkId) {
|
||||
order = dataService.getOrders().find(o => (o as any).depositPaymentLinkId === linkId || (o as any).deliveryPaymentLinkId === linkId) || null;
|
||||
}
|
||||
|
||||
const effectiveLinkId = linkId || (order as any)?.depositPaymentLinkId || (order as any)?.deliveryPaymentLinkId;
|
||||
|
||||
if (!effectiveLinkId) {
|
||||
return NextResponse.json({ success: false, error: 'No se encontró link de pago asociado a la orden' }, { status: 404 });
|
||||
}
|
||||
|
||||
const boldRes = await getBoldPaymentStatus(effectiveLinkId);
|
||||
|
||||
if (!boldRes.success) {
|
||||
return NextResponse.json({ success: false, error: boldRes.error }, { status: 500 });
|
||||
}
|
||||
|
||||
const isPaid = boldRes.status === 'PAID';
|
||||
|
||||
// Si la transacción está pagada y la orden aún no está marcada como pagada, actualizar inmediatamente
|
||||
if (isPaid && order && !(order as any).depositPaid) {
|
||||
const isDeposit = effectiveLinkId === (order as any).depositPaymentLinkId;
|
||||
const depositAmount = order.depositAmount || 10000;
|
||||
const newBalance = Math.max(0, order.totalAmount - depositAmount);
|
||||
|
||||
const updates: Record<string, any> = {
|
||||
depositPaid: true,
|
||||
depositPaidAt: new Date().toISOString(),
|
||||
balanceDue: newBalance,
|
||||
status: 'deposit_received',
|
||||
};
|
||||
|
||||
dataService.updateOrder(order.id, updates);
|
||||
await updateOrderInAppwrite(order.id, updates).catch(e => console.warn('Appwrite update warn:', e));
|
||||
|
||||
// Notificar a Carlos por WhatsApp
|
||||
try {
|
||||
const adminMsg = (
|
||||
`⚡ *¡ANTICIPO DE SEPARACIÓN CONFIRMADO VÍA BOLD!*\n\n` +
|
||||
`📦 *Orden:* #${order.id}\n` +
|
||||
`👤 *Cliente:* ${order.customerName} (+57 ${order.customerPhone})\n` +
|
||||
`📍 *Ciudad:* ${order.cityName}\n` +
|
||||
`💳 *Anticipo Recibido:* ${formatCOP(depositAmount)} COP (Confirmado Bold)\n` +
|
||||
`🤝 *Saldo a Cobrar Contraentrega:* ${formatCOP(newBalance)} COP\n\n` +
|
||||
`🚀 *Estado:* Despacho autorizado y habilitado para alistamiento en bodega.`
|
||||
);
|
||||
await sendWhatsAppMessage('573167455877', adminMsg);
|
||||
|
||||
// Notificar al cliente
|
||||
if (order.customerPhone) {
|
||||
const clientMsg = (
|
||||
`🎉 *¡Tu anticipo de separación fue confirmado con éxito!*\n\n` +
|
||||
`Hola *${order.customerName}*, recibimos correctamente tu pago de *${formatCOP(depositAmount)} COP* a través de Bold.\n\n` +
|
||||
`📦 Tu orden *#${order.id}* ya se encuentra en alistamiento y empaque para despacho a *${order.cityName}*.\n` +
|
||||
`🤝 *Saldo restante a pagar contraentrega:* *${formatCOP(newBalance)} COP* al recibir en tu puerta.\n\n` +
|
||||
`¡Muchas gracias por tu compra en DripZone!`
|
||||
);
|
||||
await sendWhatsAppMessage(order.customerPhone, clientMsg);
|
||||
}
|
||||
} catch (waErr) {
|
||||
console.warn('Error al enviar WhatsApp de confirmación Bold:', waErr);
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
status: boldRes.status,
|
||||
isPaid,
|
||||
order: order ? dataService.getOrderById(order.id) : null,
|
||||
});
|
||||
} catch (error: any) {
|
||||
return NextResponse.json({ success: false, error: error.message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
104
dripzone-app/src/app/api/webhook/bold/route.ts
Normal file
104
dripzone-app/src/app/api/webhook/bold/route.ts
Normal file
@ -0,0 +1,104 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { dataService } from '@/lib/data-service';
|
||||
import { updateOrderInAppwrite, getOrdersFromAppwrite } from '@/lib/server-appwrite';
|
||||
import { sendWhatsAppMessage } from '@/lib/evolution-api';
|
||||
import { formatCOP } from '@/lib/pricing-engine';
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const rawBody = await request.text();
|
||||
let body: any = {};
|
||||
try {
|
||||
body = JSON.parse(rawBody);
|
||||
} catch {
|
||||
return NextResponse.json({ success: false, error: 'JSON inválido' }, { status: 400 });
|
||||
}
|
||||
|
||||
console.log('[Bold Webhook Received]:', JSON.stringify(body));
|
||||
|
||||
const eventType = body.type;
|
||||
const subject = body.subject || body.reference || body.order_id;
|
||||
|
||||
if (eventType === 'SALE_APPROVED') {
|
||||
// Extraer el ID de la orden (puede venir con sufijo -DEP o -BAL)
|
||||
const cleanOrderId = subject
|
||||
? subject.replace(/-DEP$/, '').replace(/-BAL$/, '')
|
||||
: null;
|
||||
|
||||
let order = cleanOrderId ? dataService.getOrderById(cleanOrderId) : null;
|
||||
if (!order && cleanOrderId) {
|
||||
try {
|
||||
const appwriteOrders = await getOrdersFromAppwrite();
|
||||
order = appwriteOrders.find(o => o.id === cleanOrderId) || null;
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
if (order) {
|
||||
const isDeliveryBalance = subject?.endsWith('-BAL');
|
||||
const depositAmount = order.depositAmount || 10000;
|
||||
const newBalance = isDeliveryBalance ? 0 : Math.max(0, order.totalAmount - depositAmount);
|
||||
|
||||
const updates: Record<string, any> = isDeliveryBalance
|
||||
? {
|
||||
deliveryPaid: true,
|
||||
deliveryPaidAt: new Date().toISOString(),
|
||||
balanceDue: 0,
|
||||
status: 'delivered',
|
||||
}
|
||||
: {
|
||||
depositPaid: true,
|
||||
depositPaidAt: new Date().toISOString(),
|
||||
balanceDue: newBalance,
|
||||
status: 'deposit_received',
|
||||
};
|
||||
|
||||
dataService.updateOrder(order.id, updates);
|
||||
await updateOrderInAppwrite(order.id, updates).catch(e => console.warn('Appwrite update warn in webhook:', e));
|
||||
|
||||
// Enviar notificación a Carlos (+57 316 745 5877)
|
||||
try {
|
||||
const adminTitle = isDeliveryBalance
|
||||
? '💰 *¡SALDO CONTRAENTREGA PAGADO 100% VÍA BOLD!*'
|
||||
: '⚡ *¡ANTICIPO DE SEPARACIÓN RECIBIDO VÍA BOLD!*';
|
||||
|
||||
const adminMsg = (
|
||||
`${adminTitle}\n\n` +
|
||||
`📦 *Orden:* #${order.id}\n` +
|
||||
`👤 *Cliente:* ${order.customerName} (+57 ${order.customerPhone})\n` +
|
||||
`📍 *Destino:* ${order.cityName}\n` +
|
||||
`💵 *Total Pedido:* ${formatCOP(order.totalAmount)} COP\n` +
|
||||
`💳 *Abonado en Bold:* ${formatCOP(isDeliveryBalance ? order.balanceDue : depositAmount)} COP\n` +
|
||||
`🤝 *Saldo Pendiente:* ${formatCOP(newBalance)} COP\n\n` +
|
||||
`✅ Transacción confirmada en pasarela Bold.`
|
||||
);
|
||||
await sendWhatsAppMessage('573167455877', adminMsg);
|
||||
|
||||
// Notificar al cliente
|
||||
if (order.customerPhone) {
|
||||
const clientMsg = isDeliveryBalance
|
||||
? (
|
||||
`🎉 *¡Tu orden ha sido pagada en su totalidad!*\n\n` +
|
||||
`Hola *${order.customerName}*, confirmamos el pago final de tu pedido *#${order.id}*.\n` +
|
||||
`¡Gracias por comprar en DripZone! Esperamos que disfrutes al máximo tus zapatillas.`
|
||||
)
|
||||
: (
|
||||
`🎉 *¡Anticipo de separación recibido con éxito!*\n\n` +
|
||||
`Hola *${order.customerName}*, tu pago de *${formatCOP(depositAmount)} COP* por Bold fue aprobado.\n` +
|
||||
`Tu pedido *#${order.id}* pasa a alistamiento inmediato. Cancelas el saldo de *${formatCOP(newBalance)} COP* al recibir en tu puerta.`
|
||||
);
|
||||
await sendWhatsAppMessage(order.customerPhone, clientMsg);
|
||||
}
|
||||
} catch (waErr) {
|
||||
console.warn('Error enviando WhatsApp en webhook de Bold:', waErr);
|
||||
}
|
||||
|
||||
return NextResponse.json({ success: true, message: 'Venta aprobada y orden actualizada' });
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.json({ success: true, message: `Evento ${eventType} registrado` });
|
||||
} catch (error: any) {
|
||||
console.error('[Bold Webhook Error]:', error);
|
||||
return NextResponse.json({ success: false, error: error.message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@ -61,6 +61,16 @@ export default function CheckoutPage() {
|
||||
const [otpLoading, setOtpLoading] = useState<boolean>(false);
|
||||
const [otpError, setOtpError] = useState<string>('');
|
||||
|
||||
// Estados para pasarela Bold & QR Bre-B
|
||||
const [boldPayment, setBoldPayment] = useState<{
|
||||
paymentLink: string;
|
||||
checkoutUrl: string;
|
||||
qrImageUrl: string;
|
||||
amount: number;
|
||||
} | null>(null);
|
||||
const [boldPaid, setBoldPaid] = useState(false);
|
||||
const [boldLoading, setBoldLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
fetch('/api/cities')
|
||||
.then((r) => r.json())
|
||||
@ -150,6 +160,30 @@ export default function CheckoutPage() {
|
||||
spread: 70,
|
||||
origin: { y: 0.6 },
|
||||
});
|
||||
|
||||
// Si la orden requiere depósito, solicitar cobro en Bold inmediatamente
|
||||
if (depositAmount > 0) {
|
||||
setBoldLoading(true);
|
||||
fetch('/api/bold/create-payment', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
orderId: data.order.id,
|
||||
amount: depositAmount,
|
||||
type: 'deposit',
|
||||
payerEmail: '',
|
||||
redirectionUrl: window.location.href,
|
||||
}),
|
||||
})
|
||||
.then((r) => r.json())
|
||||
.then((bData) => {
|
||||
if (bData.success) {
|
||||
setBoldPayment(bData);
|
||||
}
|
||||
})
|
||||
.catch((err) => console.error('Error generando cobro Bold:', err))
|
||||
.finally(() => setBoldLoading(false));
|
||||
}
|
||||
} else {
|
||||
alert(data.error || 'Error al registrar pedido.');
|
||||
}
|
||||
@ -161,6 +195,28 @@ export default function CheckoutPage() {
|
||||
}
|
||||
};
|
||||
|
||||
// Escuchador en tiempo real del estado de pago en Bold
|
||||
useEffect(() => {
|
||||
if (!boldPayment?.paymentLink || boldPaid || !orderCompleted?.id) return;
|
||||
|
||||
const interval = setInterval(async () => {
|
||||
try {
|
||||
const res = await fetch(`/api/bold/status?linkId=${boldPayment.paymentLink}&orderId=${orderCompleted.id}`);
|
||||
const data = await res.json();
|
||||
if (data.success && data.isPaid) {
|
||||
setBoldPaid(true);
|
||||
confetti({
|
||||
particleCount: 150,
|
||||
spread: 90,
|
||||
origin: { y: 0.5 },
|
||||
});
|
||||
}
|
||||
} catch (_) {}
|
||||
}, 3000);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [boldPayment, boldPaid, orderCompleted]);
|
||||
|
||||
const handleSubmitOrder = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!customerName || !customerPhone || !address) {
|
||||
@ -279,41 +335,99 @@ export default function CheckoutPage() {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Bold.co or Nequi Separation Box */}
|
||||
{/* Bold.co QR Bre-B and Separation Box */}
|
||||
{orderCompleted.depositAmount > 0 ? (
|
||||
<div className="bg-neutral-950 border border-white/10 rounded-2xl p-4 text-left space-y-2.5">
|
||||
<div className="flex items-center space-x-2 text-white font-bold text-xs uppercase">
|
||||
<QrCode className="w-4 h-4 text-white" />
|
||||
<span>Paso 2: Realiza la separación de $10.000 COP</span>
|
||||
boldPaid ? (
|
||||
<div className="bg-emerald-950/60 border border-emerald-500/50 rounded-2xl p-5 text-center space-y-3 shadow-lg animate-in zoom-in-95">
|
||||
<div className="w-12 h-12 bg-emerald-500/20 text-emerald-400 rounded-full flex items-center justify-center mx-auto">
|
||||
<CheckCircle2 className="w-7 h-7" />
|
||||
</div>
|
||||
<p className="text-xs text-neutral-300">
|
||||
Paga de inmediato con <strong>Link de Pago Bold.co</strong> (PSE / Tarjeta / Nequi) o transfiere a nuestra cuenta oficial:
|
||||
<h4 className="text-base font-black text-emerald-300">¡Anticipo de $10.000 COP Confirmado con Éxito!</h4>
|
||||
<p className="text-xs text-neutral-300 leading-relaxed">
|
||||
Tu pago fue procesado correctamente mediante <strong>Bold</strong>. Tu orden ha sido autorizada para alistamiento prioritario en bodega y despacho inmediato.
|
||||
</p>
|
||||
<div className="bg-black/50 p-3 rounded-xl border border-emerald-500/30 text-xs flex justify-between items-center">
|
||||
<span className="text-neutral-400">🤝 Saldo a Pagar Contraentrega:</span>
|
||||
<strong className="text-emerald-400 font-mono text-sm">
|
||||
{formatCOP(Math.max(0, orderCompleted.totalAmount - (orderCompleted.depositAmount || 10000)))} COP
|
||||
</strong>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="bg-neutral-950 border border-purple-500/40 rounded-2xl p-4 sm:p-5 text-left space-y-3.5 shadow-xl">
|
||||
<div className="flex items-center justify-between border-b border-white/10 pb-2.5">
|
||||
<div className="flex items-center space-x-2 text-white font-bold text-xs uppercase">
|
||||
<QrCode className="w-4 h-4 text-purple-400" />
|
||||
<span>Paso 2: Anticipo de Separación ($10.000 COP)</span>
|
||||
</div>
|
||||
<span className="bg-purple-500/20 text-purple-300 border border-purple-500/40 text-[10px] px-2.5 py-0.5 rounded-full font-bold">
|
||||
QR Bre-B / Bold
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<p className="text-xs text-neutral-300 leading-relaxed">
|
||||
Escanea el <strong>Código QR Bre-B</strong> desde <strong>Bancolombia, Nequi, Daviplata</strong> o cualquier app bancaria, o toca el botón para pagar en línea:
|
||||
</p>
|
||||
|
||||
{boldLink && (
|
||||
{boldPayment?.qrImageUrl ? (
|
||||
<div className="flex flex-col items-center justify-center gap-2 bg-white p-4 rounded-2xl shadow-md border-2 border-purple-500/30">
|
||||
<img
|
||||
src={boldPayment.qrImageUrl}
|
||||
alt="Código QR Bre-B Bold"
|
||||
className="w-48 h-48 sm:w-56 sm:h-56 object-contain rounded-lg"
|
||||
/>
|
||||
<div className="text-center space-y-0.5">
|
||||
<p className="text-xs font-black text-neutral-900">
|
||||
Total a Pagar: $10.000 COP
|
||||
</p>
|
||||
<p className="text-[10px] font-semibold text-neutral-600">
|
||||
⚡ Compatible con Bre-B e Interoperabilidad Bancaria
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
) : boldLoading ? (
|
||||
<div className="py-8 text-center text-xs text-purple-400 font-bold animate-pulse">
|
||||
Generando Código QR Bre-B con Bold...
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="space-y-2">
|
||||
{boldPayment?.checkoutUrl && (
|
||||
<a
|
||||
href={boldLink}
|
||||
href={boldPayment.checkoutUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="w-full bg-indigo-600 hover:bg-indigo-500 text-white font-black text-xs py-2.5 px-3 rounded-xl flex items-center justify-center gap-1.5 shadow-md transition-all"
|
||||
className="w-full bg-gradient-to-r from-purple-600 to-indigo-600 hover:from-purple-500 hover:to-indigo-500 text-white font-black text-xs py-3 px-4 rounded-xl flex items-center justify-center gap-2 shadow-lg transition-all"
|
||||
>
|
||||
<CreditCard className="w-4 h-4" />
|
||||
<span>Pagar $10.000 COP con Bold.co (PSE / Tarjetas)</span>
|
||||
<span>Pagar $10.000 con Bold (PSE / Tarjeta / Nequi)</span>
|
||||
<ExternalLink className="w-3.5 h-3.5" />
|
||||
</a>
|
||||
)}
|
||||
|
||||
<div className="bg-neutral-900 p-2.5 rounded-xl border border-neutral-700 text-xs font-mono font-bold text-white">
|
||||
📱 Nequi / Daviplata: <strong>{storeSettings?.nequiAccount || '316 745 5877'}</strong> ({storeSettings?.accountHolder || 'Carlos Andrés Lasso'})
|
||||
<div className="bg-neutral-900/90 p-2.5 rounded-xl border border-neutral-800 text-xs font-mono text-neutral-300 flex justify-between items-center">
|
||||
<span>📱 Transferencia Directa:</span>
|
||||
<strong className="text-white">Nequi: {storeSettings?.nequiAccount || '316 745 5877'}</strong>
|
||||
</div>
|
||||
<p className="text-[11px] text-emerald-400 font-semibold">
|
||||
🤝 Cancelas el saldo de <strong>{formatCOP(orderCompleted.balanceDue)}</strong> al recibir el paquete en tu casa.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="pt-2 border-t border-white/10 flex justify-between items-center text-xs">
|
||||
<span className="text-neutral-400">🤝 Saldo restante contraentrega:</span>
|
||||
<span className="text-emerald-400 font-bold font-mono">
|
||||
{formatCOP(Math.max(0, orderCompleted.totalAmount - (orderCompleted.depositAmount || 10000)))} COP
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-center gap-1.5 text-[10px] text-purple-300/80 font-medium text-center">
|
||||
<div className="w-2 h-2 rounded-full bg-purple-400 animate-ping" />
|
||||
<span>Esperando confirmación en tiempo real...</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
<div className="bg-neutral-950 border border-white/10 rounded-2xl p-4 text-left space-y-2">
|
||||
<p className="text-xs text-neutral-300 font-semibold">
|
||||
Tu pedido quedó programado para entrega contraentrega en 24 horas. ¡Nos pondremos en contacto contigo por WhatsApp!
|
||||
¡Cliente Frecuente! Tu pedido quedó programado con 100% Pago Contraentrega al recibir en tu puerta.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
164
dripzone-app/src/lib/bold-service.ts
Normal file
164
dripzone-app/src/lib/bold-service.ts
Normal file
@ -0,0 +1,164 @@
|
||||
/**
|
||||
* Servicio Oficial de Integración de Pagos con Bold para DripZone
|
||||
* Soporta: QR Bre-B (Interoperable), PSE, Nequi, Botón Bancolombia y Tarjetas
|
||||
*/
|
||||
import crypto from 'crypto';
|
||||
|
||||
export interface BoldConfig {
|
||||
environment: 'sandbox' | 'production';
|
||||
identityKey: string;
|
||||
secretKey: string;
|
||||
apiBaseUrl: string;
|
||||
}
|
||||
|
||||
export function getBoldConfig(): BoldConfig {
|
||||
const env = (process.env.BOLD_ENVIRONMENT || 'sandbox').toLowerCase() as 'sandbox' | 'production';
|
||||
|
||||
if (env === 'production') {
|
||||
return {
|
||||
environment: 'production',
|
||||
identityKey: process.env.BOLD_PROD_IDENTITY_KEY || 'xPlwpPJaT7J604ZY5upk0s9ZZu54p4UGQqrZnU0OzU0',
|
||||
secretKey: process.env.BOLD_PROD_SECRET_KEY || 'Yyzm4vZjWe_N_I_5c1g7Dw',
|
||||
apiBaseUrl: 'https://integrations.api.bold.co',
|
||||
};
|
||||
}
|
||||
|
||||
// Por defecto: Modo pruebas (Sandbox)
|
||||
return {
|
||||
environment: 'sandbox',
|
||||
identityKey: process.env.BOLD_SANDBOX_IDENTITY_KEY || 'GatyXH-WVfX1pv17z8GAoNekbqps8Mdxdvj0yRg4BSk',
|
||||
secretKey: process.env.BOLD_SANDBOX_SECRET_KEY || 'BAFlCPYr6UFtQCvObJxTIA',
|
||||
apiBaseUrl: 'https://integrations.api.bold.co',
|
||||
};
|
||||
}
|
||||
|
||||
export interface CreatePaymentParams {
|
||||
orderId: string;
|
||||
amount: number;
|
||||
description: string;
|
||||
payerEmail?: string;
|
||||
redirectionUrl?: string;
|
||||
type?: 'deposit' | 'delivery_balance';
|
||||
}
|
||||
|
||||
export interface BoldPaymentResult {
|
||||
success: boolean;
|
||||
paymentLink?: string;
|
||||
checkoutUrl?: string;
|
||||
qrImageUrl?: string;
|
||||
orderId?: string;
|
||||
amount?: number;
|
||||
environment?: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Crea un link de cobro en Bold con soporte para QR Bre-B y todos los métodos digitales
|
||||
*/
|
||||
export async function createBoldPayment(params: CreatePaymentParams): Promise<BoldPaymentResult> {
|
||||
const config = getBoldConfig();
|
||||
const { orderId, amount, description, payerEmail, redirectionUrl } = params;
|
||||
|
||||
try {
|
||||
const payload: Record<string, any> = {
|
||||
amount_type: 'CLOSE',
|
||||
amount: {
|
||||
currency: 'COP',
|
||||
total_amount: Math.round(amount),
|
||||
tip_amount: 0,
|
||||
},
|
||||
reference: `${orderId}${params.type === 'delivery_balance' ? '-BAL' : '-DEP'}`,
|
||||
description: description || `Pago DripZone #${orderId}`,
|
||||
};
|
||||
|
||||
if (payerEmail) {
|
||||
payload.payer_email = payerEmail;
|
||||
}
|
||||
|
||||
if (redirectionUrl) {
|
||||
payload.redirection_url = redirectionUrl;
|
||||
}
|
||||
|
||||
const res = await fetch(`${config.apiBaseUrl}/online/link/v1`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `x-api-key ${config.identityKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
if (!res.ok || (data.errors && data.errors.length > 0)) {
|
||||
const errorMsg = data.errors?.[0]?.detail || `Error HTTP ${res.status} al crear cobro en Bold`;
|
||||
console.error('[Bold Service Error]:', errorMsg, data);
|
||||
return { success: false, error: errorMsg };
|
||||
}
|
||||
|
||||
const paymentLink = data.payload?.payment_link;
|
||||
const checkoutUrl = data.payload?.url || `https://checkout.bold.co/payment/${paymentLink}`;
|
||||
|
||||
// Generar URL del Código QR de alta definición optimizado para escanear en móviles y pantallas
|
||||
const qrImageUrl = `https://api.qrserver.com/v1/create-qr-code/?size=350x350&margin=10&data=${encodeURIComponent(checkoutUrl)}`;
|
||||
|
||||
return {
|
||||
success: true,
|
||||
paymentLink,
|
||||
checkoutUrl,
|
||||
qrImageUrl,
|
||||
orderId,
|
||||
amount,
|
||||
environment: config.environment,
|
||||
};
|
||||
} catch (error: any) {
|
||||
console.error('[Bold Service Exception]:', error);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Consulta el estado en tiempo real de una transacción en Bold
|
||||
*/
|
||||
export async function getBoldPaymentStatus(paymentLinkId: string): Promise<{
|
||||
success: boolean;
|
||||
status?: 'ACTIVE' | 'PROCESSING' | 'PAID' | 'REJECTED' | 'CANCELLED' | 'EXPIRED' | string;
|
||||
raw?: any;
|
||||
error?: string;
|
||||
}> {
|
||||
const config = getBoldConfig();
|
||||
|
||||
try {
|
||||
const res = await fetch(`${config.apiBaseUrl}/online/link/v1/${paymentLinkId}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Authorization': `x-api-key ${config.identityKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
cache: 'no-store',
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
if (!res.ok) {
|
||||
return { success: false, error: data?.errors?.[0]?.detail || `Error HTTP ${res.status}` };
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
status: data.status,
|
||||
raw: data,
|
||||
};
|
||||
} catch (error: any) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calcula la firma criptográfica SHA-256 para integridad de transacciones
|
||||
*/
|
||||
export function calculateBoldSignature(orderId: string, amount: number, currency: string = 'COP'): string {
|
||||
const config = getBoldConfig();
|
||||
const rawString = `${orderId}${Math.round(amount)}${currency}${config.secretKey}`;
|
||||
return crypto.createHash('sha256').update(rawString).digest('hex');
|
||||
}
|
||||
168
scripts/deploy_vps.py
Normal file
168
scripts/deploy_vps.py
Normal file
@ -0,0 +1,168 @@
|
||||
import paramiko
|
||||
import os
|
||||
import urllib.request
|
||||
import json
|
||||
import ssl
|
||||
import sys
|
||||
import io
|
||||
import time
|
||||
|
||||
if sys.platform == 'win32':
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
|
||||
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8', errors='replace')
|
||||
|
||||
HOST = "161.97.93.156"
|
||||
USER = "carlos"
|
||||
PASSWORD = "Innova1978+*?"
|
||||
REMOTE_BASE = "/home/carlos/apps/dripzone-app"
|
||||
LOCAL_BASE = r"c:\Users\Carlos\dev\almeira-web\dripzone-app"
|
||||
|
||||
EVOLUTION_API_URL = "https://evolution.almeira.com.co"
|
||||
EVOLUTION_API_KEY = "EE49029EEB85-4568-8F81-C9FDFC6E2943"
|
||||
INSTANCE_NAME = "AutomataAlmeira"
|
||||
TARGET_PHONE = "573167455877"
|
||||
APK_URL = "https://dripzone.almeira.com.co/DripZone.apk"
|
||||
|
||||
FILES_TO_SYNC = [
|
||||
"src/lib/data-service.ts",
|
||||
"src/lib/evolution-api.ts",
|
||||
"src/lib/bold-service.ts",
|
||||
"src/app/api/bold/create-payment/route.ts",
|
||||
"src/app/api/bold/status/route.ts",
|
||||
"src/app/api/webhook/bold/route.ts",
|
||||
"src/app/api/whatsapp-media/route.ts",
|
||||
"src/app/api/users/route.ts",
|
||||
"src/app/checkout/page.tsx",
|
||||
"src/components/store/CartDrawer.tsx",
|
||||
"src/components/store/ProductModal.tsx",
|
||||
"src/components/custom-requests/CustomRequestModal.tsx",
|
||||
"docker-compose.prod.yml",
|
||||
".env.local",
|
||||
"public/DripZone.apk",
|
||||
]
|
||||
|
||||
ctx = ssl.create_default_context()
|
||||
ctx.check_hostname = False
|
||||
ctx.verify_mode = ssl.CERT_NONE
|
||||
|
||||
def main():
|
||||
print("[1/5] Conectando a VPS 161.97.93.156...")
|
||||
ssh = paramiko.SSHClient()
|
||||
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
ssh.connect(hostname=HOST, username=USER, password=PASSWORD, timeout=30)
|
||||
sftp = ssh.open_sftp()
|
||||
|
||||
print("[2/5] Subiendo archivos modificados por SFTP...")
|
||||
for rel_path in FILES_TO_SYNC:
|
||||
local_path = os.path.join(LOCAL_BASE, rel_path.replace("/", os.sep))
|
||||
remote_path = f"{REMOTE_BASE}/{rel_path}"
|
||||
remote_dir = os.path.dirname(remote_path)
|
||||
|
||||
# Ensure remote directory exists
|
||||
parts = remote_dir.split('/')
|
||||
cur = ""
|
||||
for p in parts:
|
||||
if not p:
|
||||
continue
|
||||
cur += "/" + p
|
||||
try:
|
||||
sftp.stat(cur)
|
||||
except:
|
||||
try:
|
||||
sftp.mkdir(cur)
|
||||
except:
|
||||
pass
|
||||
|
||||
if os.path.exists(local_path):
|
||||
size_kb = os.path.getsize(local_path) / 1024
|
||||
print(f" -> Subiendo {rel_path} ({size_kb:.1f} KB)...")
|
||||
sftp.put(local_path, remote_path)
|
||||
else:
|
||||
print(f" [ALERTA] Archivo local no encontrado: {local_path}")
|
||||
|
||||
sftp.close()
|
||||
print(" [OK] Archivos sincronizados en VPS.")
|
||||
|
||||
print("[3/5] Reconstruyendo y reiniciando contenedor en VPS (docker compose build)...")
|
||||
cmd_build = f"cd {REMOTE_BASE} && docker compose -f docker-compose.prod.yml up -d --build"
|
||||
stdin, stdout, stderr = ssh.exec_command(cmd_build)
|
||||
out = stdout.read().decode('utf-8', errors='replace')
|
||||
err = stderr.read().decode('utf-8', errors='replace')
|
||||
print("Salida Build:\n", out[-1500:] if len(out) > 1500 else out)
|
||||
if err.strip():
|
||||
print("Avisos Build:\n", err[-800:] if len(err) > 800 else err)
|
||||
|
||||
# Copiar APK a contenedores y permisos
|
||||
cmd_copy = f"""
|
||||
docker cp {REMOTE_BASE}/public/DripZone.apk dripzone-production:/app/public/DripZone.apk || true
|
||||
docker cp {REMOTE_BASE}/public/DripZone.apk bfmnoga3lb0ckomhdmxe0rco-021053217798:/app/public/DripZone.apk || true
|
||||
docker exec dripzone-production ls -lh /app/public/DripZone.apk || true
|
||||
"""
|
||||
stdin, stdout, stderr = ssh.exec_command(cmd_copy)
|
||||
print("Copiar APK:\n", stdout.read().decode('utf-8', errors='replace'))
|
||||
|
||||
ssh.close()
|
||||
|
||||
print("[4/5] Verificando salud de la plataforma web y descarga del APK...")
|
||||
time.sleep(3)
|
||||
try:
|
||||
req = urllib.request.Request(APK_URL, method='HEAD')
|
||||
with urllib.request.urlopen(req, context=ctx, timeout=15) as res:
|
||||
size_mb = int(res.headers.get('Content-Length', 0)) / (1024 * 1024)
|
||||
print(f" [OK] APK disponible en {APK_URL} (HTTP {res.status}, {size_mb:.2f} MB)")
|
||||
except Exception as e:
|
||||
print(f" [WARN] Verificación APK: {e}")
|
||||
|
||||
print("[5/5] Enviando notificación detallada y archivo APK al WhatsApp de Carlos (+57 316 745 5877)...")
|
||||
message_text = (
|
||||
"🚀 *DripZone v1.5 - Pasarela de Pagos Bold & QR Bre-B Integrados*\n\n"
|
||||
"Hola Carlos, se ha completado e implementado en producción la integración integral de la pasarela oficial *Bold*:\n\n"
|
||||
"💳 *1. Cobro de Separación con QR Bre-B y Pasarela Bold:*\n"
|
||||
"• Al registrar el primer pedido, la plataforma genera automáticamente una orden de cobro en Bold por *$10.000 COP*.\n"
|
||||
"• Tanto en la Web como en la App Nativa se renderiza en vivo el *Código QR Bre-B*, permitiendo escanear y pagar al instante desde Bancolombia, Nequi, Daviplata, Dale o cualquier app bancaria interoperable.\n"
|
||||
"• Incluye botón directo para pagar con tarjeta de crédito/débito o PSE en el checkout alojado seguro de Bold.\n\n"
|
||||
"⚡ *2. Acreditación Automática en Tiempo Real:*\n"
|
||||
"• El sistema cuenta con sondeo continuo (polling de 3 segundos) y Webhook (`/api/webhook/bold`) para capturar la aprobación del pago inmediatamente.\n"
|
||||
"• En cuanto el cliente paga, la pantalla confirma el abono en verde al instante, la orden pasa a estado `deposit_received`, se resta automáticamente el saldo a pagar contraentrega y se te envía notificación inmediata con los datos del pago.\n\n"
|
||||
"🔑 *3. Modo de Pruebas Activo (Sandbox):*\n"
|
||||
"• La pasarela está operando actualmente con las llaves de prueba de la API (`GatyXH...` / `BAFlCP...`) para validar el ciclo completo con transacciones simuladas sin costo.\n"
|
||||
"• Las llaves de producción ya quedaron preconfiguradas en el servidor y listas para encender en cuanto des la orden.\n\n"
|
||||
"📲 *4. App Nativa Android v1.5 Sincronizada:*\n"
|
||||
"• Nuevo APK compilado con el módulo de lectura de QR Bre-B, botón a Bold y confirmación reactiva en vivo.\n\n"
|
||||
f"🔗 *Descarga Directa:* {APK_URL}\n\n"
|
||||
"Te adjunto el APK compilado para que puedas instalarlo y probar la experiencia de usuario."
|
||||
)
|
||||
|
||||
req_text = urllib.request.Request(
|
||||
f"{EVOLUTION_API_URL}/message/sendText/{INSTANCE_NAME}",
|
||||
data=json.dumps({"number": TARGET_PHONE, "text": message_text, "delay": 500}).encode("utf-8"),
|
||||
headers={"apikey": EVOLUTION_API_KEY, "Content-Type": "application/json"}
|
||||
)
|
||||
try:
|
||||
with urllib.request.urlopen(req_text, context=ctx, timeout=30) as resp:
|
||||
print(" [OK] Mensaje de texto enviado a WhatsApp (Status:", resp.status, ")")
|
||||
except Exception as e:
|
||||
print(" [Error texto WhatsApp]:", e)
|
||||
|
||||
req_media = urllib.request.Request(
|
||||
f"{EVOLUTION_API_URL}/message/sendMedia/{INSTANCE_NAME}",
|
||||
data=json.dumps({
|
||||
"number": TARGET_PHONE,
|
||||
"mediatype": "document",
|
||||
"mimetype": "application/vnd.android.package-archive",
|
||||
"fileName": "DripZone-Native-v1.5.apk",
|
||||
"caption": "📲 *DripZone Native Kotlin v1.5 - Pasarela Bold & QR Bre-B*",
|
||||
"media": APK_URL
|
||||
}).encode("utf-8"),
|
||||
headers={"apikey": EVOLUTION_API_KEY, "Content-Type": "application/json"}
|
||||
)
|
||||
try:
|
||||
with urllib.request.urlopen(req_media, context=ctx, timeout=180) as resp:
|
||||
print(" [OK] Documento APK enviado a WhatsApp (Status:", resp.status, ")")
|
||||
except Exception as e:
|
||||
print(" [Error envío documento APK]:", e)
|
||||
|
||||
print("\n✅ [PROCESO COMPLETADO SATISFACTORIAMENTE]")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Loading…
Reference in New Issue
Block a user