ÿØÿà JFIF    ÿÛ „  ( %"1!%)+...383,7(-.+  -+++--++++---+-+-----+---------------+---+-++7-----ÿÀ  ß â" ÿÄ     ÿÄ H    !1AQaq"‘¡2B±ÁÑð#R“Ò Tbr‚²á3csƒ’ÂñDS¢³$CÿÄ   ÿÄ %  !1AQa"23‘ÿÚ   ? ôÿ ¨pŸªáÿ —åYõõ\?àÒü©ŠÄï¨pŸªáÿ —åYõõ\?àÓü©ŠÄá 0Ÿªáÿ Ÿå[úƒ ú®ði~TÁbqÐ8OÕpÿ ƒOò¤Oè`–RÂáœá™êi€ßÉ< FtŸI“öÌ8úDf´°å}“¾œ6  öFá°y¥jñÇh†ˆ¢ã/ÃÐ:ªcÈ "Y¡ðÑl>ÿ ”ÏËte:qž\oäŠe÷󲍷˜HT4&ÿ ÓÐü6ö®¿øþßèô Ÿ•7Ñi’•j|“ñì>b…þS?*Óôÿ ÓÐü*h¥£ír¶ü UãS炟[AÐaè[ûª•õ&õj?†Éö+EzP—WeÒírJFt ‘BŒ†Ï‡%#tE Øz ¥OÛ«!1›üä±Í™%ºÍãö]°î(–:@<‹ŒÊö×òÆt¦ãº+‡¦%ÌÁ²h´OƒJŒtMÜ>ÀÜÊw3Y´•牋4ǍýʏTì>œú=Íwhyë,¾Ôò×õ¿ßÊa»«þˆѪQ|%6ž™A õ%:øj<>É—ÿ Å_ˆCbõ¥š±ý¯Ýƒï…¶|RëócÍf溪“t.СøTÿ *Ä¿-{†çàczůŽ_–^XþŒ±miB[X±d 1,é”zEù»& î9gœf™9Ð'.;—™i}!ôšåîqêÛ٤ёý£½ÆA–àôe"A$˝Úsäÿ ÷Û #°xŸëí(l »ý3—¥5m! rt`†0~'j2(]S¦¦kv,ÚÇ l¦øJA£Šƒ J3E8ÙiŽ:cÉžúeZ°€¯\®kÖ(79«Ž:¯X”¾³Š&¡* ….‰Ž(ÜíŸ2¥ª‡×Hi²TF¤ò[¨íÈRëÉ䢍mgÑ.Ÿ<öäS0í„ǹÁU´f#Vß;Õ–…P@3ío<ä-±»Ž.L|kªÀê›fÂ6@»eu‚|ÓaÞÆŸ…¨ááå>åŠ?cKü6ùTÍÆ”†sĤÚ;H2RÚ†õ\Ö·Ÿn'¾ ñ#ºI¤Å´%çÁ­‚â7›‹qT3Iï¨ÖÚ5I7Ë!ÅOóŸ¶øÝñØôת¦$Tcö‘[«Ö³šÒ';Aþ ¸èíg A2Z"i¸vdÄ÷.iõ®§)¿]¤À†–‡É&ä{V¶iŽ”.Ó×Õÿ û?h¬Mt–íª[ÿ Ñÿ ÌV(í}=ibÔ¡›¥¢±b Lô¥‡piη_Z<‡z§èŒ)iÖwiÇ 2hÙ3·=’d÷8éŽ1¦¸c¤µ€7›7Ø ð\á)} ¹fËí›pAÃL%âc2 í§æQz¿;T8sæ°qø)QFMð‰XŒÂ±N¢aF¨…8¯!U  Z©RÊ ÖPVÄÀÍin™Ì-GˆªÅËŠ›•zË}º±ŽÍFò¹}Uw×#ä5B¤{î}Ð<ÙD é©¤&‡ïDbàÁôMÁ." ¤‡ú*õ'VŽ|¼´Úgllº¼klz[Æüï÷Aób‡Eÿ dÑ»Xx9ÃÜ£ÁT/`¼¸vI±Ýµ·Ë‚“G³þ*Ÿû´r|*}<¨îºœ @¦mÄ’M¹”.œ«Y–|6ÏU¤jç¥ÕÞqO ˜kDÆÁ¨5ÿ š;ÐЦ¦€GÙk \ –Þ=â¼=SͧµªS°ÚÍpÜãQűÀõ¬?ÃÁ1Ñ•õZà?hóœ€ L¦l{Y*K˜Ù›zc˜–ˆâ ø+¾ ­-Ök¥%ùEÜA'}ˆ><ÊIè“bpÍ/qÞâvoX€w,\úªò6Z[XdÒæ­@Ö—€$òJí#é>'°Ú ôª˜<)4ryÙ£|óAÅn5žêŸyÒäMÝ2{"}‰–¤l÷ûWX\l¾Á¸góÉOÔ /óñB¤f¸çñ[.P˜ZsÊË*ßT܈§QN¢’¡¨§V¼(Üù*eÕ“”5T¨‹Âê¥FŒã½Dü[8'Ò¥a…Ú¶k7a *•›¼'Ò·\8¨ª\@\õ¢¦íq+DÙrmÎ…_ªæ»ŠÓœ¡¯’Ré9MÅ×D™lælffc+ŒÑ,ý™ÿ ¯þǤ=Å’Á7µ÷ÚÛ/“Ü€ñýã¼àí¾ÕÑ+ƒ,uµMâÀÄbm:ÒÎPæ{˜Gz[ƒ¯«® KHà`ߨŠéí¯P8Aq.C‰ à€kòpj´kN¶qô€…Õ,ÜNŠª-­{Zö’æû44‰sŽè‰îVíRœÕm" 6?³D9¡ÇTíÅꋇ`4«¸ÝÁô ï’ýorqКÇZ«x4Žâéþuïf¹µö[P ,Q£éaX±`PÉÍZ ¸äYúg üAx ’6Lê‚xÝÓ*äQ  Ï’¨hÍ =²,6ï#rÃ<¯–£»ƒ‹,–ê•€ aÛsñ'%Æ"®ÛüìBᝠHÚ3ß°©$“XnœÖ’î2ËTeûìxîß ¦å¿çÉ ðK§þ{‘t‚Ϋ¬jéîZ[ ”š7L¥4VÚCE×]m¤Øy”ä4-dz£œ§¸x.*ãÊÊ b÷•h:©‡¦s`BTÁRû¾g⻩‹jø sF¢àJøFl‘È•Xᓁà~*j¯ +(ÚÕ6-£¯÷GŠØy‚<Ç’.F‹Hœw(+)ÜÜâÈzÄäT§FߘãÏ;DmVœ3Àu@mÚüXÝü•3B¨òÌÁÛ<·ÃÜ z,Ì@õÅ·d2]ü8s÷IôÞ¯^Ç9¢u„~ëAŸï4«M? K]­ÅàPl@s_ p:°¬ZR”´›JC[CS.h‹ƒïËœ«Æ]–÷ó‚wR×k7X‰k›‘´ù¦=¡«‰¨¨Â')—71ó’c‡Ðúµ `é.{§p¹ój\Ž{1h{o±Ý=áUÊïGÖŒõ–-BÄm+AZX¶¡ ïHðæ¥JmÙ;…䡟ˆ¦ ° äšiÉg«$üMk5¤L“’çÊvïâï ,=f“"íἊ5ô¬x6{ɏžID0e¸vçmi'︧ºð9$ò¹÷*£’9ÿ ²TÔ…×>JV¥}Œ}$p[bÔ®*[jzS*8 ”·T›Í–ñUîƒwo$áè=LT™ç—~ô·¤ÈÚ$榍q‰„+´kFm)ž‹©i–ËqÞŠ‰à¶ü( ‚•§ •°ò·‡#5ª•µÊ﯅¡X¨šÁ*F#TXJÊ ušJVÍ&=iÄs1‚3•'fý§5Ñ<=[íÞ­ PÚ;ѱÌ_~Ä££8rÞ ²w;’hDT°>ÈG¬8Á²ÚzŽ®ò®qZcqJêäÞ-ö[ܘbň±çb“ж31²n×iƒðÕ;1¶þÉ ªX‰,ßqÏ$>•î íZ¥Z 1{ç൵+ƒÕµ¥°T$§K]á»Ûï*·¤tMI’ÂZbŽÕiÒ˜}bÓ0£ª5›¨ [5Ž^ÝœWøÂÝh° ¢OWun£¤5 a2Z.G2³YL]jåtì”ä ÁÓ‘%"©<Ôúʰsº UZvä‡ÄiÆÒM .÷V·™ø#kèýiíÌ–ª)µT[)BˆõÑ xB¾B€ÖT¨.¥~ð@VĶr#¸ü*åZNDŽH;âi ],©£öØpù(šºãö¼T.uCê•4@ÿ GÕÛ)Cx›®0ø#:ÏðFÒbR\(€€Ä®fã4Þ‰Fä¯HXƒÅ,†öEÑÔÜ]Öv²?tLÃvBY£ú6Êu5ÅAQ³1‘’¬x–HŒÐ‡ ^ ¸KwJôÖŽ5×CÚ¨vÜ«/B0$×k°=ðbÇ(Ï)w±A†Á† 11Í=èQšµ626ŒÜ/`G«µ<}—-Ö7KEHÈÉðóȤmݱû±·ø«Snmá=“䫚mݱŸ¡¶~ó·“äUóJæúòB|E LêŽy´jDÔ$G¢þÐñ7óR8ýÒ…Ç› WVe#·Ÿ p·Fx~•ݤF÷0Èÿ K¯æS<6’¡WШ; ´ÿ ¥Êø\Òuî†åÝ–VNœkÒ7oòX¨Á­Ø÷FÎÑä±g÷ÿ M~Çî=p,X´ ÝÌÚÅ‹’ÃjÖ.ØöÏñ qïQ¤ÓZE†° =6·]܈ s¸>v•Ž^Ý\wq9r‰Î\¸¡kURÒ$­*‹Nq?Þª*!sŠÆ:TU_u±T+øX¡ ®¹¡,ÄâÃBTsÜ$Ø›4m椴zÜK]’’›Pƒ @€#â˜`é¹=I‡fiV•Ôî“nRm+µFPOhÍ0B£ €+¬5c v•:P'ÒyÎ ‰V~‚Ó†ÖuókDoh$å\*ö%Ю=£«…aȼ½÷Û.-½VŒŠ¼'lyî±1¬3ó#ÞE¿ÔS¤gV£m›=§\û"—WU¤ÚǼÿ ÂnÁGŒÃ ‚õN D³õNÚíŒÕ;HôyÄÈ©P¹Ä{:?R‘Ô¨âF÷ø£bÅó® JS|‚R÷ivýáâ€Æé¡è³´IئÑT!§˜•ت‚¬â@q€wnïCWÄ@JU€ê¯m6]Ï:£âx'+ÒðXvÓ¦Úm=–´7œ $ì“B£~p%ÕŸUþ« N@¼üï~w˜ñø5®—'Ôe»¤5ã//€ž~‰Tþ›Å7•#¤× Íö pÄ$ùeåì*«ÓŠEØWEÈsßg ¦ûvžSsLpºÊW–âµEWöˬH; ™!CYõZ ÃÄf æ#1W. \uWâ\,\Çf j’<qTbên›Î[vxx£ë 'ö¨1›˜ÀM¼Pÿ H)ƒêêŒA7s,|F“ 꺸k³9Ìö*ç®;Ö!Ö$Eiž•¹ÒÚ†ýóéÝû¾ÕS®ó$’NÝäŸz¤5r¦ãÄÃD÷Üø!°ø‡Ô&@m™Ì^Ãä­d q5Lnÿ N;.6½·N|#ä"1Nƒx“ã<3('&ñßt  ~ªu”1Tb㫨9ê–›–bìd$ߣ=#ÕãÒmU¯eí$EFù5ýYô櫨æì™Ç—±ssM]·á¿0ÕåJRÓªîiƒ+O58ÖñªŠÒx" \µâá¨i’¤i —Ö ” M+M¤ë9‚‰A¦°Qõ¾ßøK~¼Ã‘g…Ö´~÷Ï[3GUœÒ½#…kàÔ®Ò”‰³·dWV‰IP‰Ú8u¹”E ÖqLj¾êÕCBš{A^Âß;–¨`¯¬ìö ˼ ×tìø.tƐm*n¨y4o&Àx¥n¦×î‡aupáÛj8¿m›è¶ã!o½;ß0y^ý×^EÑ¿ÒjzŒ­)vÚÑnÄL …^ªô× ‡—‚3k Îý­hï]içå–îÏ*÷ñþ»Ô CÒjøjÍznˆ´ ¹#b'Fô‹ ‰v¥'’à'T´ƒHýÍ%M‰ ƒ&ÆÇŒï1 ‘ –Þ ‰i¬s žR-Ÿ kЬá¬7:þ 0ŒÅÒÕ/aÙ¬ÃÝ#Úøœ ©aiVc‰. ¹¦ãµ” ›Yg¦›ÆÎýº°f³7ƒhá·¸­}&D9¡ÂsÉÙÞèŠõØàC™¨ñbFC|´Ü(ŸƒÚÒ-%»'a Ì¿)ËÇn¿úÿ ÞŽX…4ÊÅH^ôΑí@ù¹Eh¶“L8Çjù ¼ÎåVªóR©Ï5uà V4lZß®=€xÖŸ–ÑÈ ÷”¨°¾__yM1tÉ?uÆþIkÄgæ@þ[¢†°XÃJ£j·:nkÅ¢u ‘}âGzö­/IµèЬ¼48q¦F°ŽR¼=ûì{´¯RýicS ÕÛ íNtÍÙï£,w4rêì®»~x(©Uñ§#Ñ&œÕ¤>ÎåÍÓ9’Ö{9eV­[Öjâ²ãu]˜å2›qÑšÕJç0€sÄ|Êëè0튔bÁ>“{×_F`Ø©ºê:µä,v¤ðfc1±"«ÔÍän1#=· Âøv~H½ÐßA¾¿Ü€Óš]Õ; I¾÷ç‚Qi†î¹9ywÔKG˜áñ zQY—§ÃÕZ07§X‚ Áh;ÁM)iÌCH-¯T‘ë|A0{Ò½LÚ–TâÖkÜ’dÀ“rmm»”جPF³ÖcbE§T€ÒxKºû’Ó®7±²(\4ŽÃ¸Uu@j™yĵ;³µ!Á¢b.W¤=mõ´êµK k ¸K^ÜÛ#p*Ü14qkZç5ïë †°5Ï%ÍÛ<Õ¤×Ô¥ê†C Õ´¼ú$ƒÖ“”]Ù¬qÞÚ[4©ý!ûÏ—Áb쳐XµA¬â~`›Çr¸8ìùÝ䫦<>ä÷«?xs´ÇÑ /á;¹øüÊÈÙà{"@Žïzâ¬[âß‚ U_<ÇŸ½4èN˜ú61®qŠu ¦þF£»äJ_ˆÙÎ~ ÞAã–݄ϗrŠD;xTž‘ô`É«…suãO`?³à™ô Lý#Íc5öoæØ‚y´´÷«ZR§<&JÇ+éâô´€i!Àˆ0æAoàðLèÖ-2ŸõW.’t^–(KÁmHµV@xÜÇy®Ñø­â^:Ú3w· 7½¹°ñ¸â¹®:',«Mœ—n­Á+Ãbš LÈ‘ÄnRÓÅœ%¦²‰¨ùQ:¤f‚ "PÕtô¸…cæl…&˜Ú˜Ôkv‹ž+vŠ,=¢v­6—Xy*¥t£«<™:“aîϲ=¦6rO]XI¿Œ÷¤zÚ­›¶ 6÷”w\d ü~v®ˆÌk«^m<ÿ ¢‰Õ\)ùºŽ;… lîÙÅEŠ®cѾ@vnMÏ,¼“ñ•ŽBxðÃzãÇç%3ˆ"}Ù•Åî> BÉú;Ò]V+P˜F_´ßé> Øše|ï‡ÄOmFæÇ ãqÞ$/xÐx­z`ï9"œÜij‚!7.\Td…9M‡•iŽ‹¾‘50ÞŽn¥ß4ÉôO ¹*í^QêËÜÇÌ8=ާs‰'ÂëÙ«á%Pú[O †ÅP¯Vsް.‰,kc¶ ¬A9n˜XÎ-ÞšN["¹QÕ‰ƒMýÁߺXJæÍaLj¾×Ãmã¾ãÚ uñÒþåQô¦¥ /ÄUx:‚ÍÜ’ Đ©ØÝ3V¨‰ÕnÐ6ó*óúK­«…c ¯U òhsý­jóÔj#,ímŒRµ«lbïUTŒÑ8†Ä0œÏr`ð¡¬É Ї ë"À² ™ 6¥ f¶ ¢ÚoܱԷ-<Àî)†a¶ž'Ú»¨TXqØæ¶÷YÄHy˜9ÈIW­YÀuMFë ºÏ’AqÌ4·/Ú †ô'i$øä­=Ä Ý|öK×40è|È6p‘0§)o¥ctî§H+CA-“ xØ|ÐXАç l8íºð3Ø:³¤¬KX¯UÿÙ # Estrategia de Notificaciones de Stock - Yuki Comics ## Resumen Ejecutivo Este documento describe la estrategia para implementar un sistema de notificaciones de stock que permita a los usuarios suscribirse para recibir avisos cuando un producto sin stock vuelva a estar disponible. El sistema se integra con la infraestructura existente de cola de correos y aprovecha la tabla `producto_stock` como historial de movimientos. ## 1. Análisis del Estado Actual ### 1.1 Infraestructura Existente **Componentes Disponibles:** - ✅ **Sistema de cola de correos** implementado y funcional - ✅ **Tabla `producto_stock`** que funciona como historial de movimientos - ✅ **Clase `Producto_stock`** con métodos para gestión de stock - ✅ **Sistema de templates** de email HTML - ✅ **Configuración SMTP** (Zoho) operativa **Puntos de Actualización de Stock:** - `backend/js/carga_js.php` - Carga de stock desde admin - `_sys/lib/controller/producto.class.php` - Creación/actualización de productos - `checkout.php` - Descuento de stock en compras ### 1.2 Oportunidades Identificadas **Casos de Uso:** - Productos agotados que vuelven a tener stock - Productos en preventa que llegan al almacén - Notificaciones personalizadas por usuario **Beneficios:** - Aumentar conversiones de productos agotados - Mejorar experiencia del usuario - Reducir pérdida de ventas por falta de stock ## 2. Arquitectura del Sistema ### 2.1 Diagrama de Arquitectura ``` ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ Frontend │ │ Backend │ │ Sistema │ │ (Avisarme) │───▶│ (Admin) │───▶│ Notificaciones│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │ │ ▼ ▼ ┌─────────────────┐ ┌─────────────────┐ │ Hook en │ │ Cola de │ │ Producto_stock│───▶│ Correos │ └─────────────────┘ └─────────────────┘ │ │ ▼ ▼ ┌─────────────────┐ ┌─────────────────┐ │ Detección │ │ Envío │ │ de Cambios │ │ Automático │ └─────────────────┘ └─────────────────┘ ``` ### 2.2 Flujo de Trabajo 1. **Suscripción**: Usuario hace click en "Avisarme" → Guardar en `stock_notifications` 2. **Actualización de Stock**: Admin carga stock → Hook detecta cambio 3. **Detección**: Sistema verifica si pasó de 0 a >0 stock 4. **Notificación**: Genera emails y los agrega a la cola 5. **Envío**: Cron job procesa y envía los correos ## 3. Estructura de Base de Datos ### 3.1 Tabla `stock_notifications` ```sql CREATE TABLE `stock_notifications` ( `notification_id` int(11) NOT NULL AUTO_INCREMENT, `usuario_id` int(11) UNSIGNED NOT NULL, `producto_id` int(11) NOT NULL, `notification_email` varchar(255) NOT NULL, `notification_status` enum('active','sent','cancelled') NOT NULL DEFAULT 'active', `notification_created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, `notification_sent_at` timestamp NULL DEFAULT NULL, PRIMARY KEY (`notification_id`), UNIQUE KEY `unique_user_product` (`usuario_id`, `producto_id`), KEY `idx_producto_status` (`producto_id`, `notification_status`), KEY `idx_created_at` (`notification_created_at`), FOREIGN KEY (`usuario_id`) REFERENCES `usuario`(`usuario_id`) ON DELETE CASCADE, FOREIGN KEY (`producto_id`) REFERENCES `producto`(`producto_id`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; ``` ### 3.2 Aprovechamiento de Estructura Existente **Tabla `producto_stock` como Historial:** - ✅ Ya registra cada cambio de stock como nuevo registro - ✅ Mantiene timestamp de cada movimiento - ✅ Permite rastrear stock anterior vs actual - ✅ No requiere tabla adicional de historial ## 4. Estrategia de Implementación ### 4.1 Estrategia Recomendada: Hook en Producto_stock::save() **Justificación:** - **Mínima modificación** del código existente - **Detección automática** de todos los cambios de stock - **Integración natural** con el sistema actual - **Fácil de mantener** y debuggear **Implementación:** ```php // En Producto_stock::save() - agregar después de la actualización exitosa if($obj->validate($obj,$id)): $result = $obj->update($obj, $id); // Hook para notificaciones de stock if($result && $_POST['stock_disponible'] > 0) { $old_stock = self::getLastStock($_POST['producto_id']); if($old_stock == 0) { // Disparar notificaciones solo si pasó de 0 a >0 StockNotification::triggerNotifications($_POST['producto_id']); } } return $result; endif; ``` ### 4.2 Puntos de Integración #### **4.2.1 Frontend - producto.php** ```php // Mostrar botón "Avisarme" cuando stock = 0
Te notificaremos por email cuando este producto esté disponible
``` #### **4.2.2 Backend - carga_js.php** ```php // En el case 'carga_stock' - agregar después de Producto_stock::save(0) if($error > 0) { // Verificar si pasó de 0 a >0 stock $old_stock = Producto_stock::getLastStock($producto_id); if($old_stock == 0 && $cantidad_actual > 0) { StockNotification::triggerNotifications($producto_id); Log::write("Stock notifications triggered for product #{$producto_id}"); } } ``` #### **4.2.3 Endpoint AJAX para Suscripciones** ```php // ajax/stock_notification__ajx.php case 'subscribe': $usuario_id = Usuario::login('usuario_id'); $producto_id = numParam('producto_id'); if($usuario_id && $producto_id) { $result = StockNotification::subscribe($usuario_id, $producto_id); echo json_encode(['success' => $result]); } break; ``` ## 5. Clases y Métodos ### 5.1 Clase StockNotification ```php class StockNotification extends Mysql { protected $tableName = "stock_notifications"; protected $primaryKey = "notification_id"; protected $fields = array( "usuario_id" => array("type" => "int", "required" => "1"), "producto_id" => array("type" => "int", "required" => "1"), "notification_email" => array("type" => "varchar", "length" => 255, "required" => "1"), "notification_status" => array("type" => "enum", "values" => "active,sent,cancelled", "required" => "0"), "notification_sent_at" => array("type" => "timestamp", "required" => "0") ); /** * Suscribir usuario a notificaciones de stock */ public static function subscribe($usuario_id, $producto_id) { $usuario = Usuario::select($usuario_id); if(!haveRows($usuario)) return false; $obj = new self(); $obj->fields['usuario_id']['value'] = $usuario_id; $obj->fields['producto_id']['value'] = $producto_id; $obj->fields['notification_email']['value'] = $usuario[0]['usuario_email']; $obj->fields['notification_status']['value'] = 'active'; if($obj->validate($obj,$id)): return $obj->update($obj, $id); else: Message::set("Por favor complete correctamente el formulario para continuar", MESSAGE_ERROR); return $obj->error; endif; } /** * Cancelar suscripción */ public static function unsubscribe($usuario_id, $producto_id) { $obj = new self(); return $obj->change($obj->tableName, "notification_status", "cancelled", "usuario_id = '{$usuario_id}' AND producto_id = '{$producto_id}'"); } /** * Obtener suscripciones activas para un producto */ public static function getActiveSubscriptions($producto_id) { $obj = new self(); $sql = "SELECT sn.*, u.usuario_nombre, u.usuario_apellido, u.usuario_email FROM {$obj->tableName} sn JOIN usuario u ON sn.usuario_id = u.usuario_id WHERE sn.producto_id = '{$producto_id}' AND sn.notification_status = 'active'"; return $obj->execute($sql); } /** * Disparar notificaciones para un producto */ public static function triggerNotifications($producto_id) { $subscriptions = self::getActiveSubscriptions($producto_id); $producto = Producto::select($producto_id); if(!haveRows($subscriptions) || !haveRows($producto)) { return 0; } $sent_count = 0; foreach($subscriptions as $subscription) { $usuario = [ 'usuario_id' => $subscription['usuario_id'], 'usuario_nombre' => $subscription['usuario_nombre'], 'usuario_apellido' => $subscription['usuario_apellido'], 'usuario_email' => $subscription['notification_email'] ]; $stock = Producto_stock::disponible($producto_id); $stock_amount = haveRows($stock) ? $stock[0]['stock_disponible'] : 0; if($stock_amount > 0) { MailQueue::sendStockNotification($usuario, $producto[0], $stock_amount); self::markAsSent($subscription['notification_id']); $sent_count++; } } Log::write("Stock notifications sent: {$sent_count} for product #{$producto_id}"); return $sent_count; } /** * Marcar notificación como enviada */ public static function markAsSent($notification_id) { $obj = new self(); $obj->change($obj->tableName, "notification_status", "sent", "notification_id = '{$notification_id}'"); $obj->change($obj->tableName, "notification_sent_at", date('Y-m-d H:i:s'), "notification_id = '{$notification_id}'"); } /** * Verificar si usuario está suscrito */ public static function isSubscribed($usuario_id, $producto_id) { $obj = new self(); $sql = "SELECT notification_id FROM {$obj->tableName} WHERE usuario_id = '{$usuario_id}' AND producto_id = '{$producto_id}' AND notification_status = 'active'"; $result = $obj->execute($sql); return haveRows($result); } } ``` ### 5.2 Extensión de Producto_stock ```php // Agregar método a la clase Producto_stock existente public static function getLastStock($producto_id) { $obj = new self(); $sql = "SELECT stock_disponible FROM producto_stock WHERE producto_id = '{$producto_id}' ORDER BY stock_id DESC LIMIT 1,1"; // El penúltimo registro $result = $obj->execute($sql); return haveRows($result) ? $result[0]['stock_disponible'] : 0; } ``` ### 5.3 Extensión de MailQueue ```php // Agregar método específico para notificaciones de stock public static function sendStockNotification($usuario, $producto, $stock_amount) { $data = [ 'usuario_nombre' => $usuario['usuario_nombre'] . ' ' . $usuario['usuario_apellido'], 'producto_nombre' => $producto['producto_nombre'], 'stock_disponible' => $stock_amount, 'producto_url' => baseURL . 'detalle.php?id=' . $producto['producto_id'], 'logo' => baseURL . "images/yuki-comics.png", 'send_date' => date('d/m/Y H:i:s') ]; $from = array("info@yukicomics.com.py" => "Yuki Comics"); $to = array($usuario['usuario_email'] => $data['usuario_nombre']); $subject = "¡Stock disponible! - {$producto['producto_nombre']}"; $template = "stock_notification_template.html"; return self::sendLater($from, $to, $subject, $template, $data, [ 'type' => 'stock_notification', 'priority' => 1, 'related_id' => $producto['producto_id'] ]); } ``` ## 6. Templates de Email ### 6.1 Template stock_notification_template.html ```html Stock Disponible - Yuki Comics
Yukicomics

¡Stock Disponible!

Hola {$usuario_nombre},

¡Buenas noticias! El producto que estabas esperando ya está disponible:

{$producto_nombre}

Stock disponible: {$stock_disponible} unidades

Fecha: {$send_date}

Este email fue enviado porque te suscribiste para recibir notificaciones de stock. Si ya no deseas recibir estas notificaciones, puedes cancelar tu suscripción desde tu perfil.

yukicomics.com.py todos los derechos reservados.
``` ## 7. Frontend Implementation ### 7.1 JavaScript para Suscripciones ```javascript // js/stock-notifications.js function subscribeToStock(productoId) { if (!confirm('¿Deseas recibir una notificación cuando este producto esté disponible?')) { return; } $.ajax({ url: baseURL + 'ajax/stock_notification__ajx.php', type: 'POST', data: { action: 'subscribe', producto_id: productoId, token: getToken('stock_notification') }, dataType: 'json', success: function(response) { if (response.success) { showMessage('Te notificaremos cuando el producto esté disponible', 'success'); updateSubscriptionButton(productoId, true); } else { showMessage('Error al suscribirte. Intenta nuevamente.', 'error'); } }, error: function() { showMessage('Error de conexión. Intenta nuevamente.', 'error'); } }); } function updateSubscriptionButton(productoId, subscribed) { const button = document.querySelector(`[onclick*="subscribeToStock(${productoId})"]`); if (button) { if (subscribed) { button.innerHTML = ' Notificación activa'; button.className = 'btn btn-success btn-block'; button.onclick = null; } } } ``` ### 7.2 Endpoint AJAX ```php // ajax/stock_notification__ajx.php false, 'message' => 'Acción no especificada']); exit; endif; $action = $_POST['action']; switch($action): case 'subscribe': if(!Usuario::isLogged()): echo json_encode(['success' => false, 'message' => 'Debes iniciar sesión']); exit; endif; $usuario_id = Usuario::login('usuario_id'); $producto_id = numParam('producto_id'); if($usuario_id && $producto_id): // Verificar que el producto no tenga stock $stock = Producto_stock::disponible($producto_id); if(haveRows($stock) && $stock[0]['stock_disponible'] > 0): echo json_encode(['success' => false, 'message' => 'El producto ya tiene stock disponible']); exit; endif; // Verificar si ya está suscrito if(StockNotification::isSubscribed($usuario_id, $producto_id)): echo json_encode(['success' => false, 'message' => 'Ya estás suscrito a este producto']); exit; endif; $result = StockNotification::subscribe($usuario_id, $producto_id); echo json_encode(['success' => $result > 0]); else: echo json_encode(['success' => false, 'message' => 'Datos inválidos']); endif; break; case 'unsubscribe': if(!Usuario::isLogged()): echo json_encode(['success' => false, 'message' => 'Debes iniciar sesión']); exit; endif; $usuario_id = Usuario::login('usuario_id'); $producto_id = numParam('producto_id'); if($usuario_id && $producto_id): $result = StockNotification::unsubscribe($usuario_id, $producto_id); echo json_encode(['success' => $result]); else: echo json_encode(['success' => false, 'message' => 'Datos inválidos']); endif; break; default: echo json_encode(['success' => false, 'message' => 'Acción no válida']); break; endswitch; ?> ``` ## 8. Dashboard de Administración ### 8.1 Vista de Notificaciones ```php // backend/stock_notifications.php 0 ? " AND (p.producto_nombre LIKE '%{$search}%')" : ""; $listing = new Listing(); $listing->pgclick("module('stock_notifications&page=%s');return!1;"); $listing = $listing->get("stock_notifications sn JOIN producto p ON sn.producto_id = p.producto_id JOIN usuario u ON sn.usuario_id = u.usuario_id", 20, "sn.*, p.producto_nombre, u.usuario_nombre, u.usuario_apellido", $page, "WHERE sn.notification_hidden = 0 {$search} ORDER BY sn.notification_created_at DESC"); ?>

Notificaciones de Stock

Total Suscripciones

Notificaciones Enviadas

Usuario Producto Email Estado Fecha Suscripción Fecha Envío
``` ## 9. Testing y Validación ### 9.1 Casos de Prueba **Caso 1: Suscripción Exitosa** 1. Usuario hace click en "Avisarme" 2. Sistema guarda suscripción en BD 3. Botón cambia a "Notificación activa" **Caso 2: Detección de Stock** 1. Admin carga stock para producto sin stock 2. Hook detecta cambio de 0 a >0 3. Sistema busca suscripciones activas 4. Genera emails en cola **Caso 3: Envío de Notificaciones** 1. Cron job procesa cola de correos 2. Envía emails de notificación 3. Marca suscripciones como enviadas **Caso 4: Prevención de Duplicados** 1. Usuario intenta suscribirse nuevamente 2. Sistema detecta suscripción existente 3. Muestra mensaje de error apropiado ### 9.2 Métricas de Validación - **Tasa de suscripción**: % de usuarios que se suscriben - **Tasa de conversión**: % de notificados que compran - **Tiempo de respuesta**: Desde carga de stock hasta envío - **Tasa de entrega**: % de emails entregados exitosamente ## 10. Mantenimiento y Monitoreo ### 10.1 Logs del Sistema ```php // Logs específicos para notificaciones de stock Log::write("Stock notification subscribed: User #{$usuario_id} for Product #{$producto_id}"); Log::write("Stock notification triggered: Product #{$producto_id} - {$sent_count} emails queued"); Log::write("Stock notification sent: {$notification_id} to {$email}"); ``` ### 10.2 Limpieza Automática ```php // Cron job para limpiar notificaciones antiguas public static function cleanupOldNotifications($days = 90) { $obj = new self(); $date = date('Y-m-d H:i:s', strtotime("-{$days} days")); $sql = "DELETE FROM {$obj->tableName} WHERE notification_created_at < '{$date}' AND notification_status = 'sent'"; return $obj->execute($sql); } ``` ### 10.3 Monitoreo de Performance - **Query optimization**: Índices en campos críticos - **Batch processing**: Para múltiples notificaciones - **Rate limiting**: Evitar spam de notificaciones ## 11. Roadmap de Implementación ### 11.1 Fase 1: Infraestructura (3 días) - [ ] Crear tabla `stock_notifications` - [ ] Implementar clase `StockNotification` - [ ] Crear template de email - [ ] Agregar método `getLastStock()` a `Producto_stock` ### 11.2 Fase 2: Backend (2 días) - [ ] Agregar hook en `Producto_stock::save()` - [ ] Implementar endpoint AJAX - [ ] Extender `MailQueue` con método específico - [ ] Testing de detección de cambios ### 11.3 Fase 3: Frontend (2 días) - [ ] Agregar botón "Avisarme" en `producto.php` - [ ] Implementar JavaScript de suscripciones - [ ] Crear dashboard de administración - [ ] Testing de flujo completo ### 11.4 Fase 4: Optimización (1 día) - [ ] Testing de performance - [ ] Optimización de queries - [ ] Documentación final - [ ] Monitoreo y métricas ## 12. Consideraciones Futuras ### 12.1 Funcionalidades Avanzadas - **Notificaciones múltiples**: Email + SMS + Push - **Personalización**: Umbral de stock personalizado - **Analytics**: Métricas de conversión detalladas - **Integración**: APIs para terceros ### 12.2 Escalabilidad - **Procesamiento paralelo**: Para grandes volúmenes - **Cache**: Para datos de productos frecuentes - **Microservicios**: Separación de responsabilidades - **CDN**: Para templates de email ## 13. Conclusión Esta estrategia proporciona una solución robusta y escalable para las notificaciones de stock, aprovechando al máximo la infraestructura existente y minimizando los cambios necesarios. El sistema es fácil de mantener, debuggear y extender, siguiendo las mejores prácticas de desarrollo PHP y arquitectura de sistemas. La implementación gradual permite validar cada componente antes de continuar, reduciendo riesgos y asegurando una integración exitosa con el sistema actual de Yuki Comics.