From 9d3fa2bf9e675a37c5f1791de53b576a807b4475 Mon Sep 17 00:00:00 2001 From: Marius Mutu Date: Thu, 28 Aug 2025 00:28:20 +0300 Subject: [PATCH] Initial commit with current state --- .gitattributes | 2 + .gitignore | 10 + CLAUDE.md | 130 +++++++ README.md | 2 + gomag-vending.prg | 602 +++++++++++++++++++++++++++++++ nfjson/nfjsoncreate.prg | 381 ++++++++++++++++++++ nfjson/nfjsonread.prg | 775 ++++++++++++++++++++++++++++++++++++++++ orders-example.json | 208 +++++++++++ products-example.json | 137 +++++++ regex.prg | 268 ++++++++++++++ run-gomag.bat | 4 + settings.ini | 17 + utils.prg | 264 ++++++++++++++ 13 files changed, 2800 insertions(+) create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 CLAUDE.md create mode 100644 README.md create mode 100644 gomag-vending.prg create mode 100644 nfjson/nfjsoncreate.prg create mode 100644 nfjson/nfjsonread.prg create mode 100644 orders-example.json create mode 100644 products-example.json create mode 100644 regex.prg create mode 100644 run-gomag.bat create mode 100644 settings.ini create mode 100644 utils.prg diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..dfe0770 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Auto detect text files and perform LF normalization +* text=auto diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f5c3899 --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +*.fxp +*.FXP +*.bak +*.BAK +*.csv +/log.* +/output/*.json +*.err +*.ERR +*.log diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..c8d07de --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,130 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +This is a Visual FoxPro 9 project that interfaces with the GoMag e-commerce API. The application retrieves both product and order data from GoMag's REST API endpoints with full pagination support and comprehensive error handling. + +## Architecture + +- **Main Application**: `gomag-vending.prg` - Primary Visual FoxPro script with pagination support +- **Utility Module**: `utils.prg` - INI file handling, logging, and helper functions +- **JSON Library**: `nfjson/` - Third-party JSON parsing library for VFP +- **Technology**: Visual FoxPro 9 with WinHttp.WinHttpRequest.5.1 for HTTP requests +- **API Integration**: GoMag REST API v1 for products and orders management + +## Core Components + +### gomag-vending.prg +Main application script with: +- Complete pagination support for products and orders +- Configurable API settings via INI file +- Comprehensive error handling and logging +- Rate limiting compliance (1-second delays between requests) +- JSON array output generation for both products and orders + +### utils.prg +Utility functions module containing: +- INI file operations (`ReadPini`, `WritePini`, `LoadSettings`) +- Logging system (`InitLog`, `LogMessage`, `CloseLog`) +- Connectivity testing (`TestConnectivity`) +- URL encoding utilities (`UrlEncode`) +- Default configuration creation (`CreateDefaultIni`) + +### settings.ini +Configuration file with sections: +- `[API]` - API endpoints, credentials, and headers +- `[PAGINATION]` - Page size limits +- `[OPTIONS]` - Feature toggles for products/orders retrieval +- `[FILTERS]` - Date range filters for orders + +## Development Commands + +### Running the Application +```foxpro +DO gomag-vending.prg +``` + +### Running from Windows Command Line +Use the provided batch file: +```cmd +run-gomag.bat +``` + +Direct execution with Visual FoxPro: +```cmd +"C:\Program Files (x86)\Microsoft Visual FoxPro 9\vfp9.exe" -T "gomag-vending.prg" +``` + +## Configuration Management + +The application uses `settings.ini` for all configuration. Key settings: + +### Required Configuration +- `ApiKey` - Your GoMag API key +- `ApiShop` - Your shop URL (e.g., "https://yourstore.gomag.ro") + +### Feature Control +- `GetProducts` - Set to "1" to retrieve products, "0" to skip +- `GetOrders` - Set to "1" to retrieve orders, "0" to skip +- `OrderDaysBack` - Number of days back to retrieve orders (default: 7) + +### Pagination +- `Limit` - Records per page (default: 100, max recommended for GoMag API) + +## API Integration Details + +### Authentication +- Header-based authentication using `Apikey` and `ApiShop` headers +- User-Agent must differ from "PostmanRuntime" + +### Endpoints +- Products: `https://api.gomag.ro/api/v1/product/read/json?enabled=1` +- Orders: `https://api.gomag.ro/api/v1/order/read/json` + +### Rate Limiting +- 1-second pause between paginated requests +- No specific READ request limitations +- POST requests limited to ~1 request per second (Leaky Bucket) + +## Directory Structure + +``` +/ +├── gomag-vending.prg # Main application +├── utils.prg # Utility functions +├── settings.ini # Configuration file +├── run-gomag.bat # Windows launcher +├── nfjson/ # JSON parsing library +│ ├── nfjsoncreate.prg +│ └── nfjsonread.prg +├── log/ # Generated log files +├── output/ # Generated JSON files +└── products-example.json # Sample API response +``` + +## Output Files + +### Generated Files +- `log/gomag_sync_YYYYMMDD_HHMMSS.log` - Detailed execution logs +- `output/gomag_all_products_YYYYMMDD_HHMMSS.json` - Complete product array +- `output/gomag_orders_last7days_YYYYMMDD_HHMMSS.json` - Orders array + +### Error Files +- `gomag_error_pageN_*.json` - Raw API responses for failed parsing +- `gomag_error_pageN_*.log` - HTTP error details with status codes + +## Key Functions + +### Main Application (gomag-vending.prg) +- `SaveProductsArray` - Converts paginated product data to JSON array +- `SaveOrdersArray` - Converts paginated order data to JSON array +- `MergeProducts` - Combines products from multiple pages +- `MergeOrdersArray` - Combines orders from multiple pages + +### Utilities (utils.prg) +- `LoadSettings` - Loads complete INI configuration into object +- `InitLog`/`LogMessage`/`CloseLog` - Comprehensive logging system +- `TestConnectivity` - Internet connection verification +- `CreateDefaultIni` - Generates template configuration file \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..c3bc679 --- /dev/null +++ b/README.md @@ -0,0 +1,2 @@ +# gomag-vending + diff --git a/gomag-vending.prg b/gomag-vending.prg new file mode 100644 index 0000000..4288b06 --- /dev/null +++ b/gomag-vending.prg @@ -0,0 +1,602 @@ +*-- Script Visual FoxPro 9 pentru accesul la GoMag API cu paginare completa +*-- Autor: Claude AI +*-- Data: 26.08.2025 + +SET SAFETY OFF +SET CENTURY ON +SET DATE DMY +SET EXACT ON +SET ANSI ON +SET DELETED ON + +*-- Setari principale +LOCAL lcApiBaseUrl, lcApiUrl, lcApiKey, lcUserAgent, lcContentType +LOCAL loHttp, lcResponse, lcJsonResponse +LOCAL laHeaders[10], lnHeaderCount +Local lcApiShop, lcCsvFileName, lcErrorResponse, lcFileName, lcLogContent, lcLogFileName, lcPath +Local lcStatusText, lnStatusCode, loError +Local lnLimit, lnCurrentPage, llHasMorePages, loAllJsonData, lnTotalPages, lnTotalProducts +Local lcOrderApiUrl, loAllOrderData, lcOrderCsvFileName, lcOrderJsonFileName +Local ldStartDate, lcStartDateStr +Local lcIniFile, loSettings +LOCAL llGetProducts, llGetOrders +PRIVATE gcAppPath, loJsonData, gcLogFile, gnStartTime, gnProductsProcessed, gnOrdersProcessed + + + +gcAppPath = ADDBS(JUSTPATH(SYS(16,0))) +SET DEFAULT TO (m.gcAppPath) +lcPath = gcAppPath + 'nfjson;' +SET PATH TO (m.lcPath) ADDITIVE + +SET PROCEDURE TO utils.prg ADDITIVE +SET PROCEDURE TO nfjsonread.prg ADDITIVE +SET PROCEDURE TO nfjsoncreate.prg ADDITIVE + +*-- Initializare logging si statistici +gnStartTime = SECONDS() +gnProductsProcessed = 0 +gnOrdersProcessed = 0 +gcLogFile = InitLog("gomag_sync") + +*-- Cream directorul output daca nu existe +LOCAL lcOutputDir +lcOutputDir = gcAppPath + "output" +IF !DIRECTORY(lcOutputDir) + MKDIR (lcOutputDir) +ENDIF + +*-- Incarcarea setarilor din fisierul INI +lcIniFile = gcAppPath + "settings.ini" + +*-- Verificare existenta fisier INI +IF !CheckIniFile(lcIniFile) + LogMessage("ATENTIE: Fisierul settings.ini nu a fost gasit!", "WARN", gcLogFile) + LogMessage("Cream un fisier settings.ini implicit...", "INFO", gcLogFile) + IF CreateDefaultIni(lcIniFile) + LogMessage("Fisier settings.ini creat cu succes.", "INFO", gcLogFile) + LogMessage("IMPORTANT: Modifica setarile din settings.ini (ApiKey, ApiShop) inainte de a rula scriptul din nou!", "INFO", gcLogFile) + RETURN .F. + ELSE + LogMessage("EROARE: Nu s-a putut crea fisierul settings.ini!", "ERROR", gcLogFile) + RETURN .F. + ENDIF +ENDIF + +*-- Incarcarea setarilor +loSettings = LoadSettings(lcIniFile) + +*-- Verificare setari obligatorii +IF EMPTY(loSettings.ApiKey) OR loSettings.ApiKey = "YOUR_API_KEY_HERE" + LogMessage("EROARE: ApiKey nu este setat in settings.ini!", "ERROR", gcLogFile) + RETURN .F. +ENDIF + +IF EMPTY(loSettings.ApiShop) OR "yourstore.gomag.ro" $ loSettings.ApiShop + LogMessage("EROARE: ApiShop nu este setat corect in settings.ini!", "ERROR", gcLogFile) + RETURN .F. +ENDIF + +*-- Configurare API din settings.ini +lcApiBaseUrl = loSettings.ApiBaseUrl +lcOrderApiUrl = loSettings.OrderApiUrl +lcApiKey = loSettings.ApiKey +lcApiShop = loSettings.ApiShop +lcUserAgent = loSettings.UserAgent +lcContentType = loSettings.ContentType +lnLimit = loSettings.Limit +llGetProducts = loSettings.GetProducts +llGetOrders = loSettings.GetOrders +lnCurrentPage = 1 && Pagina de start +llHasMorePages = .T. && Flag pentru paginare +loAllJsonData = NULL && Obiect pentru toate datele + +*-- Calculare data pentru ultimele X zile (din settings.ini) +ldStartDate = DATE() - loSettings.OrderDaysBack +lcStartDateStr = TRANSFORM(YEAR(ldStartDate)) + "-" + ; + RIGHT("0" + TRANSFORM(MONTH(ldStartDate)), 2) + "-" + ; + RIGHT("0" + TRANSFORM(DAY(ldStartDate)), 2) + +*-- Verificare daca avem WinHttp disponibil +TRY + loHttp = CREATEOBJECT("WinHttp.WinHttpRequest.5.1") +CATCH TO loError + LogMessage("Eroare la crearea obiectului WinHttp: " + loError.Message, "ERROR", gcLogFile) + RETURN .F. +ENDTRY +*-- SECTIUNEA PRODUSE - se executa doar daca llGetProducts = .T. +IF llGetProducts + LogMessage("[PRODUCTS] Starting product retrieval", "INFO", gcLogFile) + + *-- Bucla pentru preluarea tuturor produselor (paginare) + loAllJsonData = CREATEOBJECT("Empty") + ADDPROPERTY(loAllJsonData, "products", CREATEOBJECT("Empty")) + ADDPROPERTY(loAllJsonData, "total", 0) + ADDPROPERTY(loAllJsonData, "pages", 0) + lnTotalProducts = 0 + + DO WHILE llHasMorePages + *-- Construire URL cu paginare + lcApiUrl = lcApiBaseUrl + "&page=" + TRANSFORM(lnCurrentPage) + "&limit=" + TRANSFORM(lnLimit) + + LogMessage("[PRODUCTS] Page " + TRANSFORM(lnCurrentPage) + " fetching...", "INFO", gcLogFile) + + *-- Configurare request + TRY + *-- Initializare request GET + loHttp.Open("GET", lcApiUrl, .F.) + + *-- Setare headers conform documentatiei GoMag + loHttp.SetRequestHeader("User-Agent", lcUserAgent) + loHttp.SetRequestHeader("Content-Type", lcContentType) + loHttp.SetRequestHeader("Accept", "application/json") + loHttp.SetRequestHeader("Apikey", lcApiKey) && Header pentru API Key + loHttp.SetRequestHeader("ApiShop", lcApiShop) && Header pentru shop URL + + *-- Setari timeout + loHttp.SetTimeouts(30000, 30000, 30000, 30000) && 30 secunde pentru fiecare + + *-- Trimitere request + loHttp.Send() + + *-- Verificare status code + lnStatusCode = loHttp.Status + lcStatusText = loHttp.StatusText + + IF lnStatusCode = 200 + *-- Success - preluare raspuns + lcResponse = loHttp.ResponseText + + *-- Parsare JSON cu nfjson + SET PATH TO nfjson ADDITIVE + loJsonData = nfJsonRead(lcResponse) + + IF !ISNULL(loJsonData) + *-- Prima pagina - setam informatiile generale + IF lnCurrentPage = 1 + LogMessage("[PRODUCTS] Analyzing JSON structure...", "INFO", gcLogFile) + LOCAL ARRAY laJsonProps[1] + lnPropCount = AMEMBERS(laJsonProps, loJsonData, 0) + FOR lnDebugIndex = 1 TO MIN(lnPropCount, 10) && Primele 10 proprietati + lcPropName = laJsonProps(lnDebugIndex) + lcPropType = TYPE('loJsonData.' + lcPropName) + LogMessage("[PRODUCTS] Property: " + lcPropName + " (Type: " + lcPropType + ")", "DEBUG", gcLogFile) + ENDFOR + + IF TYPE('loJsonData.total') = 'C' OR TYPE('loJsonData.total') = 'N' + loAllJsonData.total = VAL(TRANSFORM(loJsonData.total)) + ENDIF + IF TYPE('loJsonData.pages') = 'C' OR TYPE('loJsonData.pages') = 'N' + loAllJsonData.pages = VAL(TRANSFORM(loJsonData.pages)) + ENDIF + LogMessage("[PRODUCTS] Total items: " + TRANSFORM(loAllJsonData.total) + " | Pages: " + TRANSFORM(loAllJsonData.pages), "INFO", gcLogFile) + ENDIF + + *-- Adaugare produse din pagina curenta + LOCAL llHasProducts, lnProductsFound + llHasProducts = .F. + lnProductsFound = 0 + + IF TYPE('loJsonData.products') = 'O' + *-- Numaram produsele din obiectul products + lnProductsFound = AMEMBERS(laProductsPage, loJsonData.products, 0) + IF lnProductsFound > 0 + DO MergeProducts WITH loAllJsonData, loJsonData + llHasProducts = .T. + LogMessage("[PRODUCTS] Found: " + TRANSFORM(lnProductsFound) + " products in page " + TRANSFORM(lnCurrentPage), "INFO", gcLogFile) + gnProductsProcessed = gnProductsProcessed + lnProductsFound + ENDIF + ENDIF + + IF !llHasProducts + LogMessage("[PRODUCTS] WARNING: No products found in JSON response for page " + TRANSFORM(lnCurrentPage), "WARN", gcLogFile) + ENDIF + + *-- Verificare daca mai sunt pagini + IF TYPE('loJsonData.pages') = 'C' OR TYPE('loJsonData.pages') = 'N' + lnTotalPages = VAL(TRANSFORM(loJsonData.pages)) + IF lnCurrentPage >= lnTotalPages + llHasMorePages = .F. + ENDIF + ELSE + *-- Daca nu avem info despre pagini, verificam daca sunt produse + IF TYPE('loJsonData.products') != 'O' + llHasMorePages = .F. + ENDIF + ENDIF + + lnCurrentPage = lnCurrentPage + 1 + + ELSE + *-- Salvare raspuns JSON raw in caz de eroare de parsare + lcFileName = "gomag_error_page" + TRANSFORM(lnCurrentPage) + "_" + DTOS(DATE()) + "_" + STRTRAN(TIME(), ":", "") + ".json" + STRTOFILE(lcResponse, lcFileName) + llHasMorePages = .F. + ENDIF + + ELSE + *-- Eroare HTTP - salvare in fisier de log + lcLogFileName = "gomag_error_page" + TRANSFORM(lnCurrentPage) + "_" + DTOS(DATE()) + "_" + STRTRAN(TIME(), ":", "") + ".log" + lcLogContent = "HTTP Error " + TRANSFORM(lnStatusCode) + ": " + lcStatusText + CHR(13) + CHR(10) + + *-- Incearca sa citesti raspunsul pentru detalii despre eroare + TRY + lcErrorResponse = loHttp.ResponseText + IF !EMPTY(lcErrorResponse) + lcLogContent = lcLogContent + "Error Details:" + CHR(13) + CHR(10) + lcErrorResponse + ENDIF + CATCH + lcLogContent = lcLogContent + "Could not read error details" + ENDTRY + + STRTOFILE(lcLogContent, lcLogFileName) + llHasMorePages = .F. + ENDIF + + CATCH TO loError + *-- Salvare erori in fisier de log pentru pagina curenta + lcLogFileName = "gomag_error_page" + TRANSFORM(lnCurrentPage) + "_" + DTOS(DATE()) + "_" + STRTRAN(TIME(), ":", "") + ".log" + lcLogContent = "Script Error on page " + TRANSFORM(lnCurrentPage) + ":" + CHR(13) + CHR(10) +; + "Error Number: " + TRANSFORM(loError.ErrorNo) + CHR(13) + CHR(10) +; + "Error Message: " + loError.Message + CHR(13) + CHR(10) +; + "Error Line: " + TRANSFORM(loError.LineNo) + STRTOFILE(lcLogContent, lcLogFileName) + llHasMorePages = .F. + ENDTRY + + *-- Pauza scurta intre cereri pentru a evita rate limiting + IF llHasMorePages + INKEY(1) && Pauza de 1 secunda + ENDIF + +ENDDO + + *-- Salvare array JSON cu toate produsele + IF !ISNULL(loAllJsonData) AND TYPE('loAllJsonData.products') = 'O' + lcJsonFileName = lcOutputDir + "\gomag_all_products_" + DTOS(DATE()) + "_" + STRTRAN(TIME(), ":", "") + ".json" + DO SaveProductsArray WITH loAllJsonData, lcJsonFileName + LogMessage("[PRODUCTS] JSON saved: " + lcJsonFileName, "INFO", gcLogFile) + *-- Calculam numarul de produse procesate + IF TYPE('loAllJsonData.products') = 'O' + LOCAL ARRAY laProducts[1] + lnPropCount = AMEMBERS(laProducts, loAllJsonData.products, 0) + gnProductsProcessed = lnPropCount + ENDIF + ENDIF + +ELSE + LogMessage("[PRODUCTS] Skipped product retrieval (llGetProducts = .F.)", "INFO", gcLogFile) +ENDIF + +*-- SECTIUNEA COMENZI - se executa doar daca llGetOrders = .T. +IF llGetOrders + LogMessage("[ORDERS] =======================================", "INFO", gcLogFile) + LogMessage("[ORDERS] RETRIEVING ORDERS FROM LAST " + TRANSFORM(loSettings.OrderDaysBack) + " DAYS", "INFO", gcLogFile) + LogMessage("[ORDERS] Start date: " + lcStartDateStr, "INFO", gcLogFile) + LogMessage("[ORDERS] =======================================", "INFO", gcLogFile) + + *-- Reinitializare pentru comenzi + lnCurrentPage = 1 + llHasMorePages = .T. + loAllOrderData = CREATEOBJECT("Empty") + ADDPROPERTY(loAllOrderData, "orders", CREATEOBJECT("Empty")) + ADDPROPERTY(loAllOrderData, "total", 0) + ADDPROPERTY(loAllOrderData, "pages", 0) + +*-- Bucla pentru preluarea comenzilor +DO WHILE llHasMorePages + *-- Construire URL cu paginare si filtrare pe data (folosind startDate conform documentatiei GoMag) + lcApiUrl = lcOrderApiUrl + "?startDate=" + lcStartDateStr + "&page=" + TRANSFORM(lnCurrentPage) + "&limit=" + TRANSFORM(lnLimit) + + LogMessage("[ORDERS] Page " + TRANSFORM(lnCurrentPage) + " fetching...", "INFO", gcLogFile) + + *-- Configurare request + TRY + *-- Initializare request GET + loHttp.Open("GET", lcApiUrl, .F.) + + *-- Setare headers conform documentatiei GoMag + loHttp.SetRequestHeader("User-Agent", lcUserAgent) + loHttp.SetRequestHeader("Content-Type", lcContentType) + loHttp.SetRequestHeader("Accept", "application/json") + loHttp.SetRequestHeader("Apikey", lcApiKey) && Header pentru API Key + loHttp.SetRequestHeader("ApiShop", lcApiShop) && Header pentru shop URL + + *-- Setari timeout + loHttp.SetTimeouts(30000, 30000, 30000, 30000) && 30 secunde pentru fiecare + + *-- Trimitere request + loHttp.Send() + + *-- Verificare status code + lnStatusCode = loHttp.Status + lcStatusText = loHttp.StatusText + + IF lnStatusCode = 200 + *-- Success - preluare raspuns + lcResponse = loHttp.ResponseText + + *-- Parsare JSON cu nfjson + SET PATH TO nfjson ADDITIVE + loJsonData = nfJsonRead(lcResponse) + + IF !ISNULL(loJsonData) + *-- Debug: Afisam structura JSON pentru prima pagina + IF lnCurrentPage = 1 + LogMessage("[ORDERS] Analyzing JSON structure...", "INFO", gcLogFile) + LOCAL ARRAY laJsonProps[1] + lnPropCount = AMEMBERS(laJsonProps, loJsonData, 0) + FOR lnDebugIndex = 1 TO MIN(lnPropCount, 10) && Primele 10 proprietati + lcPropName = laJsonProps(lnDebugIndex) + lcPropType = TYPE('loJsonData.' + lcPropName) + LogMessage("[ORDERS] Property: " + lcPropName + " (Type: " + lcPropType + ")", "DEBUG", gcLogFile) + ENDFOR + ENDIF + + *-- Prima pagina - setam informatiile generale din metadata + IF lnCurrentPage = 1 + IF TYPE('loJsonData.total') = 'C' OR TYPE('loJsonData.total') = 'N' + loAllOrderData.total = VAL(TRANSFORM(loJsonData.total)) + ENDIF + IF TYPE('loJsonData.pages') = 'C' OR TYPE('loJsonData.pages') = 'N' + loAllOrderData.pages = VAL(TRANSFORM(loJsonData.pages)) + ENDIF + LogMessage("[ORDERS] Total orders: " + TRANSFORM(loAllOrderData.total), "INFO", gcLogFile) + LogMessage("[ORDERS] Total pages: " + TRANSFORM(loAllOrderData.pages), "INFO", gcLogFile) + ENDIF + + *-- Adaugare comenzi din pagina curenta + *-- API-ul GoMag returneaza obiect cu metadata si orders + LOCAL llHasOrders, lnOrdersFound + llHasOrders = .F. + lnOrdersFound = 0 + + *-- Verificam daca avem obiectul orders + IF TYPE('loJsonData.orders') = 'O' + *-- Numaram comenzile din obiectul orders + lnOrdersFound = AMEMBERS(laOrdersPage, loJsonData.orders, 0) + IF lnOrdersFound > 0 + *-- Mergem comenzile din pagina curenta + DO MergeOrdersArray WITH loAllOrderData, loJsonData + llHasOrders = .T. + LogMessage("[ORDERS] Found: " + TRANSFORM(lnOrdersFound) + " orders in page " + TRANSFORM(lnCurrentPage), "INFO", gcLogFile) + gnOrdersProcessed = gnOrdersProcessed + lnOrdersFound + ENDIF + ENDIF + + IF !llHasOrders + LogMessage("[ORDERS] WARNING: No orders found in JSON response for page " + TRANSFORM(lnCurrentPage), "WARN", gcLogFile) + ENDIF + + *-- Verificare daca mai sunt pagini folosind metadata + IF TYPE('loJsonData.pages') = 'C' OR TYPE('loJsonData.pages') = 'N' + lnTotalPages = VAL(TRANSFORM(loJsonData.pages)) + IF lnCurrentPage >= lnTotalPages + llHasMorePages = .F. + ENDIF + ELSE + *-- Fallback: verifica daca am primit mai putin decat limita + IF !llHasOrders OR lnOrdersFound < lnLimit + llHasMorePages = .F. + ENDIF + ENDIF + + lnCurrentPage = lnCurrentPage + 1 + + ELSE + *-- Salvare raspuns JSON raw in caz de eroare de parsare + lcFileName = "gomag_order_error_page" + TRANSFORM(lnCurrentPage) + "_" + DTOS(DATE()) + "_" + STRTRAN(TIME(), ":", "") + ".json" + STRTOFILE(lcResponse, lcFileName) + llHasMorePages = .F. + ENDIF + + ELSE + *-- Eroare HTTP - salvare in fisier de log + lcLogFileName = "gomag_order_error_page" + TRANSFORM(lnCurrentPage) + "_" + DTOS(DATE()) + "_" + STRTRAN(TIME(), ":", "") + ".log" + lcLogContent = "HTTP Error " + TRANSFORM(lnStatusCode) + ": " + lcStatusText + CHR(13) + CHR(10) + + *-- Incearca sa citesti raspunsul pentru detalii despre eroare + TRY + lcErrorResponse = loHttp.ResponseText + IF !EMPTY(lcErrorResponse) + lcLogContent = lcLogContent + "Error Details:" + CHR(13) + CHR(10) + lcErrorResponse + ENDIF + CATCH + lcLogContent = lcLogContent + "Could not read error details" + ENDTRY + + STRTOFILE(lcLogContent, lcLogFileName) + llHasMorePages = .F. + ENDIF + + CATCH TO loError + *-- Salvare erori in fisier de log pentru pagina curenta + lcLogFileName = "gomag_order_error_page" + TRANSFORM(lnCurrentPage) + "_" + DTOS(DATE()) + "_" + STRTRAN(TIME(), ":", "") + ".log" + lcLogContent = "Script Error on page " + TRANSFORM(lnCurrentPage) + ":" + CHR(13) + CHR(10) +; + "Error Number: " + TRANSFORM(loError.ErrorNo) + CHR(13) + CHR(10) +; + "Error Message: " + loError.Message + CHR(13) + CHR(10) +; + "Error Line: " + TRANSFORM(loError.LineNo) + STRTOFILE(lcLogContent, lcLogFileName) + llHasMorePages = .F. + ENDTRY + + *-- Pauza scurta intre cereri pentru a evita rate limiting + IF llHasMorePages + INKEY(1) && Pauza de 1 secunda + ENDIF + +ENDDO + + *-- Salvare array JSON cu toate comenzile + IF !ISNULL(loAllOrderData) AND TYPE('loAllOrderData.orders') = 'O' + lcOrderJsonFileName = lcOutputDir + "\gomag_orders_last7days_" + DTOS(DATE()) + "_" + STRTRAN(TIME(), ":", "") + ".json" + DO SaveOrdersArray WITH loAllOrderData, lcOrderJsonFileName + LogMessage("[ORDERS] JSON file created: " + lcOrderJsonFileName, "INFO", gcLogFile) + ENDIF + +ELSE + LogMessage("[ORDERS] Skipped order retrieval (llGetOrders = .F.)", "INFO", gcLogFile) +ENDIF + +*-- Curatare +loHttp = NULL + +*-- Inchidere logging cu statistici finale +CloseLog(gnStartTime, gnProductsProcessed, gnOrdersProcessed, gcLogFile) + + +*-- Functie pentru salvarea array-ului de produse in format JSON +PROCEDURE SaveProductsArray +PARAMETERS tloAllData, tcFileName + +LOCAL lcJsonContent, lnPropCount, lnIndex, lcPropName, loProduct + +*-- Incepe array-ul JSON +lcJsonContent = "[" + CHR(13) + CHR(10) + +*-- Verifica daca avem produse +IF TYPE('tloAllData.products') = 'O' + lnPropCount = AMEMBERS(laProducts, tloAllData.products, 0) + + FOR lnIndex = 1 TO lnPropCount + lcPropName = laProducts(lnIndex) + loProduct = EVALUATE('tloAllData.products.' + lcPropName) + + IF TYPE('loProduct') = 'O' + *-- Adauga virgula pentru elementele anterioare + IF lnIndex > 1 + lcJsonContent = lcJsonContent + "," + CHR(13) + CHR(10) + ENDIF + + *-- Serializeaza produsul cu nfjsoncreate + lcProductJson = nfJsonCreate(loProduct, .F.) + lcJsonContent = lcJsonContent + " " + lcProductJson + ENDIF + ENDFOR +ENDIF + +*-- Inchide array-ul JSON +lcJsonContent = lcJsonContent + CHR(13) + CHR(10) + "]" + +*-- Salveaza fisierul +STRTOFILE(lcJsonContent, tcFileName) + +ENDPROC + +*-- Functie pentru salvarea array-ului de comenzi in format JSON +PROCEDURE SaveOrdersArray +PARAMETERS tloAllData, tcFileName + +LOCAL lcJsonContent, lnPropCount, lnIndex, lcPropName, loOrder + +*-- Incepe array-ul JSON +lcJsonContent = "[" + CHR(13) + CHR(10) + +*-- Verifica daca avem comenzi +IF TYPE('tloAllData.orders') = 'O' + lnPropCount = AMEMBERS(laOrders, tloAllData.orders, 0) + + FOR lnIndex = 1 TO lnPropCount + lcPropName = laOrders(lnIndex) + loOrder = EVALUATE('tloAllData.orders.' + lcPropName) + + IF TYPE('loOrder') = 'O' + *-- Adauga virgula pentru elementele anterioare + IF lnIndex > 1 + lcJsonContent = lcJsonContent + "," + CHR(13) + CHR(10) + ENDIF + + *-- Serializeaza comanda cu nfjsoncreate + lcOrderJson = nfJsonCreate(loOrder, .F.) + lcJsonContent = lcJsonContent + " " + lcOrderJson + ENDIF + ENDFOR +ENDIF + +*-- Inchide array-ul JSON +lcJsonContent = lcJsonContent + CHR(13) + CHR(10) + "]" + +*-- Salveaza fisierul +STRTOFILE(lcJsonContent, tcFileName) + +ENDPROC + +*-- Functie pentru unirea produselor din toate paginile (versiune simpla) +PROCEDURE MergeProducts +PARAMETERS tloAllData, tloPageData + +LOCAL lnPropCount, lnIndex, lcPropName, loProduct + +*-- Verifica daca avem produse in pagina curenta +IF TYPE('tloPageData.products') = 'O' + *-- Itereaza prin toate produsele din pagina + lnPropCount = AMEMBERS(laPageProducts, tloPageData.products, 0) + + FOR lnIndex = 1 TO lnPropCount + lcPropName = laPageProducts(lnIndex) + loProduct = EVALUATE('tloPageData.products.' + lcPropName) + + IF TYPE('loProduct') = 'O' + *-- Adauga produsul la colectia principala + ADDPROPERTY(tloAllData.products, lcPropName, loProduct) + ENDIF + ENDFOR +ENDIF + +ENDPROC + +*-- Functie pentru unirea comenzilor din array direct (structura GoMag) +PROCEDURE MergeOrdersArray +PARAMETERS tloAllData, tloPageData + +LOCAL lnPropCount, lnIndex, lcPropName, loOrder + +*-- Verifica daca avem comenzi in pagina curenta +IF TYPE('tloPageData.orders') = 'O' + *-- Itereaza prin toate comenzile din pagina (array direct) + lnPropCount = AMEMBERS(laPageOrders, tloPageData.orders, 0) + + FOR lnIndex = 1 TO lnPropCount + lcPropName = laPageOrders(lnIndex) + loOrder = EVALUATE('tloPageData.orders.' + lcPropName) + + IF TYPE('loOrder') = 'O' + *-- Folosim ID-ul comenzii ca nume proprietate, sau un index secvential + LOCAL lcOrderId + lcOrderId = "" + IF TYPE('loOrder.id') = 'C' + lcOrderId = "order_" + loOrder.id + ELSE + lcOrderId = "order_" + TRANSFORM(lnIndex) + ENDIF + + *-- Adauga comanda la colectia principala + ADDPROPERTY(tloAllData.orders, lcOrderId, loOrder) + ENDIF + ENDFOR +ENDIF + +ENDPROC + +*-- Functiile utilitare au fost mutate in utils.prg + +*-- Scriptul cu paginare completa pentru preluarea tuturor produselor si comenzilor +*-- Caracteristici principale: +*-- - Paginare automata pentru toate produsele si comenzile +*-- - Pauze intre cereri pentru respectarea rate limiting +*-- - Salvare JSON array-uri pure (fara metadata de paginare) +*-- - Utilizare nfjsoncreate pentru generare JSON corecta +*-- - Logging separat pentru fiecare pagina in caz de eroare +*-- - Afisare progres in timpul executiei + +*-- INSTRUCTIUNI DE UTILIZARE: +*-- 1. Modifica settings.ini cu setarile tale: +*-- - ApiKey: cheia ta API de la GoMag +*-- - ApiShop: URL-ul magazinului tau +*-- - GetProducts: 1 pentru a prelua produse, 0 pentru a sari peste +*-- - GetOrders: 1 pentru a prelua comenzi, 0 pentru a sari peste +*-- - OrderDaysBack: numarul de zile pentru preluarea comenzilor +*-- 2. Ruleaza scriptul - va prelua doar ce ai selectat +*-- 3. Verifica fisierele JSON generate cu array-uri pure + +*-- Script optimizat cu salvare JSON array-uri - verificati fisierele generate \ No newline at end of file diff --git a/nfjson/nfjsoncreate.prg b/nfjson/nfjsoncreate.prg new file mode 100644 index 0000000..ac69ac6 --- /dev/null +++ b/nfjson/nfjsoncreate.prg @@ -0,0 +1,381 @@ +*------------------------------------------------------------------- +* Created by Marco Plaza @vfp2Nofox +* ver 1.100 - 24/02/2016 +* enabled collection processing +* ver 1.101 - 24/02/2016 +* solved indentation on nested collections +* ver 1.110 -11/03/2016 +* -added support for collections inside arrays +* -user can pass aMemembersFlag value +* ( since Json is intended for DTO creation default value is 'U' ) +* check amembers topic on vfp help file for usage +* changed cr to crlf +* Added Json validation ; throws error for invalid Json. +* ver 1.120 +* encode control characters ( chr(0) ~ chr(31) ) +*----------------------------------------------------------------------- +Parameters ovfp,FormattedOutput,nonullarrayitem,crootName,aMembersFlag + +#Define crlf Chr(13)+Chr(10) + +Private All + +aMembersFlag = Evl(m.aMembersFlag,'U') + +esarray = Type('oVfp',1) = 'A' +esobjeto = Vartype(m.ovfp) = 'O' + +If !m.esarray And !m.esobjeto + Error 'must supply a vfp object/array' +Endif + +_nivel = Iif( Cast(m.formattedOutput As l ) , 1, -1) + +Do Case + Case esarray + + ojson = Createobject('empty') + + AddProperty(ojson,'array(1)') + Acopy(ovfp,ojson.Array) + cjson = procobject(ojson,.F.,m.nonullarrayitem,m.aMembersFlag) + cjson = Substr( m.cjson,At('[',m.cjson)) + + + Case Type('oVfp.BaseClass')='C' And ovfp.BaseClass = 'Collection' + cjson = procobject(ovfp,.T.,m.nonullarrayitem,m.aMembersFlag) + + crootName = Evl(m.crootName,'collection') + cjson = '{"'+m.crootName+collTagName(ovfp)+'": '+cjson+'}'+Iif(FormattedOutput,crlf,'')+'}' + + Otherwise + cjson = '{'+procobject(ovfp,.F.,m.nonullarrayitem,m.aMembersFlag)+'}' + +Endcase + + +Return Ltrim(cjson) + +*---------------------------------------- +Function collTagName(thiscoll) + *---------------------------------------- + Return Iif( m.thiscoll.Count > 0 And !Empty( m.thiscoll.GetKey(1) ), '_kv_collection','_kl_collection' ) + + *---------------------------------------------------------------------------------- +Function procobject(obt,iscollection,nonullarrayitem,aMembersFlag) + *---------------------------------------------------------------------------------- + + If Isnull(obt) + Return 'null' + Endif + + Private All Except _nivel + + este = '' + + xtabs = nivel(2) + + bc = Iif(Type('m.obt.class')='C',m.obt.Class,'?') + + iscollection = bc = 'Collection' + + If m.iscollection + + + este = este+'{ '+xtabs + xtabs = nivel(2) + este = este+'"collectionitems": ['+xtabs + + procCollection(obt,m.nonullarrayitem,m.aMembersFlag) + + xtabs = nivel(-2) + este = este+xtabs+']' + + Else + + Amembers(am,m.obt,0,m.aMembersFlag) + + If Vartype(m.am) = 'U' + xtabs=m.nivel(-2) + Return '' + Endif + + + nm = Alen(am) + + For x1 = 1 To m.nm + + Var = Lower(am(m.x1)) + + este = m.este+Iif(m.x1>1,',','')+m.xtabs + + este = m.este+["]+Strtran(m.var,'_vfpsafe_','')+[":] + + esobjeto = Type('m.obt.&Var')='O' + + If Type('m.obt.&var') = 'U' + este = m.este+["unable to evaluate expression"] + Loop + Endif + + esarray = Type('m.obt.&Var',1) = 'A' + + Do Case + + Case m.esarray + + procarray(obt,m.var,m.nonullarrayitem) + + Case m.esobjeto + + thiso=m.obt.&Var + + bc = Iif(Type('m.thiso.class')='C',m.thiso.Class,'?') + + If bc = 'Collection' + + este = Rtrim(m.este,1,'":')+ collTagName( m.thiso )+'":' + + este = m.este+procobject(m.obt.&Var,.T.,m.nonullarrayitem,m.aMembersFlag)+[}] + + Else + + este = m.este+[{]+procobject(m.obt.&Var,.F.,m.nonullarrayitem,m.aMembersFlag)+[}] + + Endif + + Otherwise + + + este = este+concatval(m.obt.&Var) + + Endcase + + Endfor + + + Endif + + xtabs = nivel(-2) + este = este+m.xtabs + + + Return m.este + + + *---------------------------------------------------- +Procedure procarray(obt,arrayName,nonullarrayitem) + *---------------------------------------------------- + nrows = Alen(m.obt.&arrayName,1) + ncols = Alen(m.obt.&arrayName,2) + bidim = m.ncols > 0 + ncols = Iif(m.ncols=0,m.nrows,m.ncols) + titems = Alen(m.obt.&arrayName) + + xtabs=nivel(2) + + este = m.este+'['+m.xtabs + nelem = 1 + + Do While nelem <= m.titems + + este = este+Iif(m.nelem>1,','+m.xtabs,'') + + If m.bidim + xtabs = nivel(2) + este = m.este+'['+m.xtabs + Endif + + For pn = m.nelem To m.nelem+m.ncols-1 + + elem = m.obt.&arrayName( m.pn ) + + este = m.este+Iif(m.pn>m.nelem,','+m.xtabs,'') + + If Vartype(m.elem) # 'O' + + If m.nelem+m.ncols-1 = 1 And Isnull(m.elem) And m.nonullarrayitem + + este = m.este+"" + + Else + este = m.este+concatval(m.elem) + + Endif + + Else + + + bc = Iif(Type('m.elem.class')='C',m.elem.Class,'?') + + If bc = 'Collection' + + este = m.este+' { "collection'+ collTagName( m.elem )+'":' + + este = m.este+procobject(m.elem ,.T.,m.nonullarrayitem,m.aMembersFlag) + + este = este + '}'+m.xtabs+'}' + + Else + + este = m.este+[{]+procobject(m.elem ,.F.,m.nonullarrayitem,m.aMembersFlag)+[}] + + Endif + + + Endif + + Endfor + + nelem = m.pn + + If m.bidim + xtabs=nivel(-2) + este = m.este+m.xtabs+']' + Endif + + Enddo + + + xtabs=nivel(-2) + + este = m.este+m.xtabs+']' + + + + + + *----------------------------- +Function nivel(N) + *----------------------------- + If m._nivel = -1 + Return '' + Else + _nivel= m._nivel+m.n + Return crlf+Replicate(' ',m._nivel) + Endif + + *----------------------------- +Function concatval(valor) + *----------------------------- + + #Define specialChars ["\/]+Chr(127)+Chr(12)+Chr(10)+Chr(13)+Chr(9)+Chr(0)+Chr(1)+Chr(2)+Chr(3)+Chr(4)+Chr(5)+Chr(6)+Chr(7)+Chr(8)+Chr(9)+Chr(10)+Chr(11)+Chr(12)+Chr(13)+Chr(14)+Chr(15)+Chr(16)+Chr(17)+Chr(18)+Chr(19)+Chr(20)+Chr(21)+Chr(22)+Chr(23)+Chr(24)+Chr(25)+Chr(26)+Chr(27)+Chr(28)+Chr(29)+Chr(30)+Chr(31) + + If Isnull(m.valor) + + Return 'null' + + Else + + + tvar = Vartype(m.valor) + ** no cambiar el orden de ejecución! + Do Case + Case m.tvar $ 'FBYINQ' + vc = Rtrim(Cast( m.valor As c(32))) + Case m.tvar = 'L' + vc = Iif(m.valor,'true','false') + Case m.tvar $ 'DT' + vc = ["]+Ttoc(m.valor,3)+["] + Case mustEncode(m.valor) + vc = ["]+escapeandencode(m.valor)+["] + Case m.tvar $ 'CVM' + vc = ["]+Rtrim(m.valor)+["] + Case m.tvar $ 'GQW' + vc = ["]+Strconv(m.valor,13)+["] + Endcase + + Return m.vc + + Endif + + *----------------------------------- +Function mustEncode(valor) + *----------------------------------- + Return Len(Chrtran(m.valor,specialChars,'')) <> Len(m.valor) + + *------------------------------- +Function escapeandencode(valun) + *------------------------------- + valun = Strtran(m.valun,'\','\\') + valun = Strtran(m.valun,'"','\"') + *valun = Strtran(m.valun,'/','\/') + + If !mustEncode(m.valun) + Return + Endif + + valun = Strtran(m.valun,Chr(127),'\b') + valun = Strtran(m.valun,Chr(12),'\f') + valun = Strtran(m.valun,Chr(10),'\n') + valun = Strtran(m.valun,Chr(13),'\r') + valun = Strtran(m.valun,Chr(9),'\t') + + If !mustEncode(m.valun) + Return + Endif + + Local x + For x = 0 To 31 + valun = Strtran(m.valun,Chr(m.x),'\u'+Right(Transform(m.x,'@0'),4)) + Endfor + + Return Rtrim(m.valun) + + + + *--------------------------------------------------------------- +Function procCollection(obt,nonullArrayItems,aMembersFlag ) + *--------------------------------------------------------------- + + Local iscollection + + With obt + + nm = .Count + + conllave = .Count > 0 And !Empty(.GetKey(1)) + + For x1 = 1 To .Count + + If conllave + elem = Createobject('empty') + AddProperty(elem,'Key', .GetKey(x1) ) + AddProperty(elem,'Value',.Item(x1)) + Else + elem = .Item(x1) + Endif + + este = este+Iif(x1>1,','+xtabs,'') + + If Vartype(elem) # 'O' + + este = este+concatval(m.elem) + + Else + + If Vartype( m.elem.BaseClass ) = 'C' And m.elem.BaseClass = 'Collection' + iscollection = .T. + este = m.este+'{ '+m.xtabs+'"collection'+collTagName(m.elem)+'" :' + xtabs = nivel(2) + Else + iscollection = .F. + m.este = m.este+'{' + Endif + + este = este+procobject(m.elem, m.iscollection , m.nonullarrayitem, m.aMembersFlag ) + + este = este+'}' + + If m.iscollection + xtabs = nivel(-2) + este = este+m.xtabs+'}' + Endif + + Endif + + Endfor + + este = Rtrim(m.este,1,m.xtabs) + + Endwith diff --git a/nfjson/nfjsonread.prg b/nfjson/nfjsonread.prg new file mode 100644 index 0000000..671f3e6 --- /dev/null +++ b/nfjson/nfjsonread.prg @@ -0,0 +1,775 @@ +*------------------------------------------------------------------- +* Created by Marco Plaza vfp2nofox@gmail.com / @vfp2Nofox +* ver 2.000 - 26/03/2016 +* ver 2.090 - 22/07/2016 : +* improved error management +* nfjsonread will return .null. for invalid json +*------------------------------------------------------------------- +Lparameters cjsonstr,isFileName,reviveCollection + +#Define crlf Chr(13)+Chr(10) + +Private All + +stackLevels=Astackinfo(aerrs) + +If m.stackLevels > 1 + calledFrom = 'called From '+aerrs(m.stackLevels-1,4)+' line '+Transform(aerrs(m.stackLevels-1,5)) +Else + calledFrom = '' +Endif + +oJson = nfJsonCreate2(cjsonstr,isFileName,reviveCollection) + +Return Iif(Vartype(m.oJson)='O',m.oJson,.Null.) + + +*------------------------------------------------------------------------- +Function nfJsonCreate2(cjsonstr,isFileName,reviveCollection) +*------------------------------------------------------------------------- +* validate parameters: + +Do Case +Case ; + Vartype(m.cjsonstr) # 'C' Or; + Vartype(m.reviveCollection) # 'L' Or ; + Vartype(m.isFileName) # 'L' + + jERROR('invalid parameter type') + +Case m.isFileName And !File(m.cjsonstr) + + jERROR('File "'+Rtrim(Left(m.cjsonstr,255))+'" does not exist') + + +Endcase + +* process json: + +If m.isFileName + cjsonstr = Filetostr(m.cjsonstr) +Endif + + +cJson = Rtrim(Chrtran(m.cjsonstr,Chr(13)+Chr(9)+Chr(10),'')) +pChar = Left(Ltrim(m.cJson),1) + + +nl = Alines(aj,m.cJson,20,'{','}','"',',',':','[',']') + +For xx = 1 To Alen(aj) + If Left(Ltrim(aj(m.xx)),1) $ '{}",:[]' Or Left(Ltrim(m.aj(m.xx)),4) $ 'true/false/null' + aj(m.xx) = Ltrim(aj(m.xx)) + Endif +Endfor + + +Try + + x = 1 + cError = '' + oStack = Createobject('stack') + + oJson = Createobject('empty') + + Do Case + Case aj(1)='{' + x = 1 + oStack.pushObject() + procstring(m.oJson) + + Case aj(1) = '[' + x = 0 + procstring(m.oJson,.T.) + + Otherwise + Error 'Invalid Json: expecting [{ received '+m.pChar + + Endcase + + + If m.reviveCollection + oJson = reviveCollection(m.oJson) + Endif + + +Catch To oerr + + strp = '' + + For Y = 1 To m.x + strp = m.strp+aj(m.y) + Endfor + + Do Case + Case oerr.ErrorNo = 1098 + + cError = ' Invalid Json: '+ m.oerr.Message+crlf+' Parsing: '+Right(m.strp,80) + +*+' program line: '+Transform(oerr.Lineno)+' array item '+Transform(m.x) + + Case oerr.ErrorNo = 2034 + + cError = ' INVALID DATE: '+crlf+' Parsing: '+Right(m.strp,80) + + + Otherwise + + cError = 'program error # '+Transform(m.oerr.ErrorNo)+crlf+m.oerr.Message+' at: '+Transform(oerr.Lineno)+crlf+' Parsing ('+Transform(m.x)+') ' + + Endcase + +Endtry + +If !Empty(m.cError) + jERROR(m.cError) +Endif + +Return m.oJson + + + +*------------------------------------------------ +Procedure jERROR( cMessage ) +*------------------------------------------------ +Error 'nfJson ('+m.calledFrom+'):'+crlf+m.cMessage +Return To nfJsonRead + + + +*-------------------------------------------------------------------------------- +Procedure procstring(obj,eValue) +*-------------------------------------------------------------------------------- +#Define cvalid 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890_' +#Define creem '_______________________________________________________________' + +Private rowpos,colpos,bidim,ncols,arrayName,expecting,arrayLevel,vari +Private expectingPropertyName,expectingValue,objectOpen + +expectingPropertyName = !m.eValue +expectingValue = m.eValue +expecting = Iif(expectingPropertyName,'"}','') +objectOpen = .T. +bidim = .F. +colpos = 0 +rowpos = 0 +arrayLevel = 0 +arrayName = '' +vari = '' +ncols = 0 + +Do While m.objectOpen + + x = m.x+1 + + Do Case + + Case m.x > m.nl + + m.x = m.nl + + If oStack.Count > 0 + Error 'expecting '+m.expecting + Endif + + Return + + Case aj(m.x) = '}' And '}' $ m.expecting + closeObject() + + Case aj(x) = ']' And ']' $ m.expecting + closeArray() + + Case m.expecting = ':' + If aj(m.x) = ':' + expecting = '' + Loop + Else + Error 'expecting : received '+aj(m.x) + Endif + + Case ',' $ m.expecting + + Do Case + Case aj(x) = ',' + expecting = Iif( '[' $ m.expecting , '[' , '' ) + Case Not aj(m.x) $ m.expecting + Error 'expecting '+m.expecting+' received '+aj(m.x) + Otherwise + expecting = Strtran(m.expecting,',','') + Endcase + + + Case m.expectingPropertyName + + If aj(m.x) = '"' + propertyName(m.obj) + Else + Error 'expecting "'+m.expecting+' received '+aj(m.x) + Endif + + + Case m.expectingValue + + If m.expecting == '[' And m.aj(m.x) # '[' + Error 'expecting [ received '+aj(m.x) + Else + procValue(m.obj) + Endif + + + Endcase + + +Enddo + + +*---------------------------------------------------------- +Function anuevoel(obj,arrayName,valasig,bidim,colpos,rowpos) +*---------------------------------------------------------- + + +If m.bidim + + colpos = m.colpos+1 + + If colpos > m.ncols + ncols = m.colpos + Endif + + Dimension obj.&arrayName(m.rowpos,m.ncols) + + obj.&arrayName(m.rowpos,m.colpos) = m.valasig + + If Vartype(m.valasig) = 'O' + procstring(obj.&arrayName(m.rowpos,m.colpos)) + Endif + +Else + + rowpos = m.rowpos+1 + Dimension obj.&arrayName(m.rowpos) + + obj.&arrayName(m.rowpos) = m.valasig + + If Vartype(m.valasig) = 'O' + procstring(obj.&arrayName(m.rowpos)) + Endif + +Endif + + +*----------------------------------------- +Function unescunicode( Value ) +*----------------------------------------- + + +noc=1 + +Do While .T. + + posunicode = At('\u',m.value,m.noc) + + If m.posunicode = 0 + Return + Endif + + If Substr(m.value,m.posunicode-1,1) = '\' And Substr(m.value,m.posunicode-2,1) # '\' + noc=m.noc+1 + Loop + Endif + + nunic = Evaluate('0x'+ Substr(m.value,m.posunicode+2,4) ) + + If Between(m.nunic,0,255) + unicodec = Chr(m.nunic) + Else + unicodec = '&#'+Transform(m.nunic)+';' + Endif + + Value = Stuff(m.value,m.posunicode,6,m.unicodec) + + +Enddo + +*----------------------------------- +Function unescapecontrolc( Value ) +*----------------------------------- + +If At('\', m.value) = 0 + Return +Endif + +* unescape special characters: + +Private aa,elem,unesc + + +Declare aa(1) +=Alines(m.aa,m.value,18,'\\','\b','\f','\n','\r','\t','\"','\/') + +unesc ='' + +#Define sustb 'bnrt/"' +#Define sustr Chr(127)+Chr(10)+Chr(13)+Chr(9)+Chr(47)+Chr(34) + +For Each elem In m.aa + + If ! m.elem == '\\' And Right(m.elem,2) = '\' + elem = Left(m.elem,Len(m.elem)-2)+Chrtran(Right(m.elem,1),sustb,sustr) + Endif + + unesc = m.unesc+m.elem + +Endfor + +Value = m.unesc + +*-------------------------------------------- +Procedure propertyName(obj) +*-------------------------------------------- + +vari='' + +Do While ( Right(m.vari,1) # '"' Or ( Right(m.vari,2) = '\"' And Right(m.vari,3) # '\\"' ) ) And Alen(aj) > m.x + x=m.x+1 + vari = m.vari+aj(m.x) +Enddo + +If Right(m.vari,1) # '"' + Error ' expecting " received '+ Right(Rtrim(m.vari),1) +Endif + +vari = Left(m.vari,Len(m.vari)-1) +vari = Iif(Isalpha(m.vari),'','_')+m.vari +vari = Chrtran( vari, Chrtran( vari, cvalid,'' ) , creem ) + +If vari = 'tabindex' + vari = '_tabindex' +Endif + + +expecting = ':' +expectingValue = .T. +expectingPropertyName = .F. + + +*------------------------------------------------------------- +Procedure procValue(obj) +*------------------------------------------------------------- + +Do Case +Case aj(m.x) = '{' + + oStack.pushObject() + + If m.arrayLevel = 0 + + AddProperty(obj,m.vari,Createobject('empty')) + + procstring(obj.&vari) + expectingPropertyName = .T. + expecting = ',}' + expectingValue = .F. + + Else + + anuevoel(m.obj,m.arrayName,Createobject('empty'),m.bidim,@colpos,@rowpos) + expectingPropertyName = .F. + expecting = ',]' + expectingValue = .T. + + Endif + + +Case aj(x) = '[' + + oStack.pushArray() + + Do Case + + Case m.arrayLevel = 0 + + arrayName = Evl(m.vari,'array') + rowpos = 0 + colpos = 0 + bidim = .F. + +#DEFINE EMPTYARRAYFLAG '_EMPTY_ARRAY_FLAG_' + + Try + AddProperty(obj,(m.arrayName+'(1)'),EMPTYARRAYFLAG) + Catch + m.arrayName = m.arrayName+'_vfpSafe_' + AddProperty(obj,(m.arrayName+'(1)'),EMPTYARRAYFLAG) + Endtry + + + Case m.arrayLevel = 1 And !m.bidim + + rowpos = 1 + colpos = 0 + ncols = 1 + + Dime obj.&arrayName(1,2) + bidim = .T. + + Endcase + + arrayLevel = m.arrayLevel+1 + + vari='' + + expecting = Iif(!m.bidim,'[]{',']') + expectingValue = .T. + expectingPropertyName = .F. + +Otherwise + + isstring = aj(m.x)='"' + x = m.x + Iif(m.isstring,1,0) + + Value = '' + + Do While .T. + + Value = m.value+m.aj(m.x) + + If m.isstring + If Right(m.value,1) = '"' And ( Right(m.value,2) # '\"' Or Right(m.value,3) = '\\' ) + Exit + Endif + Else + If Right(m.value,1) $ '}],' And ( Left(Right(m.value,2),1) # '\' Or Left(Right(Value,3),2) = '\\') + Exit + Endif + Endif + + If m.x < Alen(aj) + x = m.x+1 + Else + Exit + Endif + + Enddo + + closeChar = Right(m.value,1) + + Value = Rtrim(m.value,1,m.closeChar) + + If Empty(Value) And Not ( m.isstring And m.closeChar = '"' ) + Error 'Expecting value received '+m.closeChar + Endif + + Do Case + + Case m.isstring + If m.closeChar # '"' + Error 'expecting " received '+m.closeChar + Endif + + Case oStack.isObject() And Not m.closeChar $ ',}' + Error 'expecting ,} received '+m.closeChar + + Case oStack.isArray() And Not m.closeChar $ ',]' + Error 'expecting ,] received '+m.closeChar + + Endcase + + + + If m.isstring + +* don't change this lines sequence!: + unescunicode(@Value) && 1 + unescapecontrolc(@Value) && 2 + Value = Strtran(m.value,'\\','\') && 3 + +** check for Json Date: + If isJsonDt( m.value ) + Value = jsonDateToDT( m.value ) + Endif + + Else + + Value = Alltrim(m.value) + + Do Case + Case m.value == 'null' + Value = .Null. + Case m.value == 'true' Or m.value == 'false' + Value = Value='true' + Case Empty(Chrtran(m.value,'-1234567890.E','')) And Occurs('.',m.value) <= 1 And Occurs('-',m.value) <= 1 And Occurs('E',m.value)<=1 + If Not 'E' $ m.value + Value = Cast( m.value As N( Len(m.value) , Iif(At('.',m.value)>0,Len(m.value)-At( '.',m.value) ,0) )) + Endif + Otherwise + Error 'expecting "|number|null|true|false| received '+aj(m.x) + Endcase + + + Endif + + + If m.arrayLevel = 0 + + + AddProperty(obj,m.vari,m.value) + + expecting = '}' + expectingValue = .F. + expectingPropertyName = .T. + + Else + + anuevoel(obj,m.arrayName,m.value,m.bidim,@colpos,@rowpos) + expecting = ']' + expectingValue = .T. + expectingPropertyName = .F. + + Endif + + expecting = Iif(m.isstring,',','')+m.expecting + + + Do Case + Case m.closeChar = ']' + closeArray() + Case m.closeChar = '}' + closeObject() + Endcase + +Endcase + + +*------------------------------ +Function closeArray() +*------------------------------ + +If oStack.Pop() # 'A' + Error 'unexpected ] ' +Endif + +If m.arrayLevel = 0 + Error 'unexpected ] ' +Endif + +arrayLevel = m.arrayLevel-1 + +If m.arrayLevel = 0 + + arrayName = '' + rowpos = 0 + colpos = 0 + + expecting = Iif(oStack.isObject(),',}','') + expectingPropertyName = .T. + expectingValue = .F. + +Else + + If m.bidim + rowpos = m.rowpos+1 + colpos = 0 + expecting = ',][' + Else + expecting = ',]' + Endif + + expectingValue = .T. + expectingPropertyName = .F. + +Endif + + + +*------------------------------------- +Procedure closeObject +*------------------------------------- + +If oStack.Pop() # 'O' + Error 'unexpected }' +Endif + +If m.arrayLevel = 0 + expecting = ',}' + expectingValue = .F. + expectingPropertyName = .T. + objectOpen = .F. +Else + expecting = ',]' + expectingValue = .T. + expectingPropertyName = .F. +Endif + + +*---------------------------------------------- +Function reviveCollection( o ) +*---------------------------------------------- + +Private All + +oConv = Createobject('empty') + +nProp = Amembers(elem,m.o,0,'U') + +For x = 1 To m.nProp + + estaVar = m.elem(x) + + esArray = .F. + esColeccion = Type('m.o.'+m.estaVar) = 'O' And Right( m.estaVar , 14 ) $ '_KV_COLLECTION,_KL_COLLECTION' And Type( 'm.o.'+m.estaVar+'.collectionitems',1) = 'A' + + Do Case + Case m.esColeccion + + estaProp = Createobject('collection') + + tv = m.o.&estaVar + + m.keyValColl = Right( m.estaVar , 14 ) = '_KV_COLLECTION' + + For T = 1 To Alen(m.tv.collectionItems) + + If m.keyValColl + esteval = m.tv.collectionItems(m.T).Value + Else + esteval = m.tv.collectionItems(m.T) + ENDIF + + IF VARTYPE(m.esteval) = 'C' AND m.esteval = emptyarrayflag + loop + ENDIF + + If Vartype(m.esteval) = 'O' Or Type('esteVal',1) = 'A' + esteval = reviveCollection(m.esteval) + Endif + + If m.keyValColl + estaProp.Add(esteval,m.tv.collectionItems(m.T).Key) + Else + estaProp.Add(m.esteval) + Endif + + Endfor + + Case Type('m.o.'+m.estaVar,1) = 'A' + + esArray = .T. + + For T = 1 To Alen(m.o.&estaVar) + + Dimension &estaVar(m.T) + + If Type('m.o.&estaVar(m.T)') = 'O' + &estaVar(m.T) = reviveCollection(m.o.&estaVar(m.T)) + Else + &estaVar(m.T) = m.o.&estaVar(m.T) + Endif + + Endfor + + Case Type('m.o.'+estaVar) = 'O' + estaProp = reviveCollection(m.o.&estaVar) + + Otherwise + estaProp = m.o.&estaVar + + Endcase + + + estaVar = Strtran( m.estaVar,'_KV_COLLECTION', '' ) + estaVar = Strtran( m.estaVar, '_KL_COLLECTION', '' ) + + Do Case + Case m.esColeccion + AddProperty(m.oConv,m.estaVar,m.estaProp) + Case m.esArray + AddProperty(m.oConv,m.estaVar+'(1)') + Acopy(&estaVar,m.oConv.&estaVar) + Otherwise + AddProperty(m.oConv,m.estaVar,m.estaProp) + Endcase + +Endfor + +Try + retCollection = m.oConv.Collection.BaseClass = 'Collection' +Catch + retCollection = .F. +Endtry + +If m.retCollection + Return m.oConv.Collection +Else + Return m.oConv +Endif + + +*---------------------------------- +Function isJsonDt( cstr ) +*---------------------------------- +Return Iif( Len(m.cstr) = 19 ; + AND Len(Chrtran(m.cstr,'01234567890:T-','')) = 0 ; + and Substr(m.cstr,5,1) = '-' ; + and Substr(m.cstr,8,1) = '-' ; + and Substr(m.cstr,11,1) = 'T' ; + and Substr(m.cstr,14,1) = ':' ; + and Substr(m.cstr,17,1) = ':' ; + and Occurs('T',m.cstr) = 1 ; + and Occurs('-',m.cstr) = 2 ; + and Occurs(':',m.cstr) = 2 ,.T.,.F. ) + + +*----------------------------------- +Procedure jsonDateToDT( cJsonDate ) +*----------------------------------- +Return Eval("{^"+m.cJsonDate+"}") + + + +****************************************** +Define Class Stack As Collection +****************************************** + +*--------------------------- + Function pushObject() +*--------------------------- + This.Add('O') + +*--------------------------- + Function pushArray() +*--------------------------- + This.Add('A') + +*-------------------------------------- + Function isObject() +*-------------------------------------- + If This.Count > 0 + Return This.Item( This.Count ) = 'O' + Else + Return .F. + Endif + + +*-------------------------------------- + Function isArray() +*-------------------------------------- + If This.Count > 0 + Return This.Item( This.Count ) = 'A' + Else + Return .F. + Endif + +*---------------------------- + Function Pop() +*---------------------------- + cret = This.Item( This.Count ) + This.Remove( This.Count ) + Return m.cret + +****************************************** +Enddefine +****************************************** + + diff --git a/orders-example.json b/orders-example.json new file mode 100644 index 0000000..bad7c6a --- /dev/null +++ b/orders-example.json @@ -0,0 +1,208 @@ +{ + "total": "399", + "page": "1", + "pages": 4, + "orders": { + "60644": { + "id": "60644", + "number": "436232189", + "date": "2025-08-27 16:32:43", + "invoice": { + "series": "", + "number": "0", + "date": "0000-00-00 00:00:00" + }, + "total": "1026.24", + "status": "Comanda noua", + "statusId": "1", + "source": "internal", + "sales_channel": "Website", + "sales_channel_marketplace": "", + "sales_agent": "", + "currency": "RON", + "observation": "", + "payment": { + "name": "Numerar/Ramburs sau Card la easybox", + "online": "0", + "completed": "0" + }, + "delivery": { + "name": "Transport gratuit", + "total": 0, + "date": "0000-00-00 00:00:00", + "lockerId": 0 + }, + "shipping": { + "company": "", + "firstname": "Liviu", + "lastname": "Stefan", + "phone": "0751013764", + "email": "liviustefan2001@gmail.com", + "address": "aleea Soarelui nr 2, casa nr 4, cartier Brates Lake", + "city": "GalaÈ›i", + "region": "Galati", + "country": "Romania", + "zipcode": null + }, + "items": [ + { + "id": "582", + "type": "product", + "sku": "8000070028685", + "ean": "8000070028685", + "name": "Lavazza Gusto Forte Vending Cafea Boabe 1kg", + "price": "69.79", + "baseprice": "78", + "vat": "11", + "quantity": "10.00" + }, + { + "id": "589", + "type": "product", + "sku": "5941623010333", + "ean": "5941623010333", + "name": "Doncafe Espresso Intense Cafea Boabe 1 kg", + "price": "56.49", + "baseprice": "62", + "vat": "11", + "quantity": "2.00" + }, + { + "id": "512", + "type": "product", + "sku": "82", + "ean": "", + "name": "Zahar Plic Lavazza 200buc", + "price": "10.99", + "baseprice": "14", + "vat": "21", + "quantity": "5.00" + }, + { + "id": "671", + "type": "product", + "sku": "312349", + "ean": "", + "name": "Palete manuale din lemn 110mm 1000buc", + "price": "7.99", + "baseprice": "10.5", + "vat": "21", + "quantity": "2.00" + }, + { + "id": "554", + "type": "product", + "sku": "8004990127091", + "ean": "8004990127091", + "name": "Ristora Ciocolată Instant 1kg", + "price": "25.99", + "baseprice": "28", + "vat": "21", + "quantity": "2.00" + }, + { + "id": "567", + "type": "product", + "sku": "8004990125530", + "ean": "8004990125530", + "name": "Prolait Topping Blue Lapte Granulat 500g", + "price": "18.49", + "baseprice": "23", + "vat": "21", + "quantity": "5.00" + } + ], + "billing": { + "firstname": "Liviu", + "lastname": "Stefan", + "phone": "0751013764", + "email": "liviustefan2001@gmail.com", + "address": "aleea Soarelui nr 2, casa nr 4, cartier Brates Lake", + "city": "GalaÈ›i", + "region": "Galati", + "country": "Romania", + "customerId": "5997" + }, + "discounts": [ + ], + "updated": "2025-08-27 16:32:43" + }, + "60643": { + "id": "60643", + "number": "436232175", + "date": "2025-08-27 16:19:29", + "invoice": { + "series": "", + "number": "0", + "date": "0000-00-00 00:00:00" + }, + "total": "359.4", + "status": "Comanda noua", + "statusId": "1", + "source": "internal", + "sales_channel": "Website", + "sales_channel_marketplace": "", + "sales_agent": "", + "currency": "RON", + "observation": "", + "payment": { + "name": "Numerar/Ramburs sau Card la easybox", + "online": "0", + "completed": "0" + }, + "delivery": { + "name": "Transport National", + "total": 30, + "date": "0000-00-00 00:00:00", + "lockerId": 0 + }, + "shipping": { + "company": "", + "firstname": "Alexandra", + "lastname": "TONE", + "phone": "0763571486", + "email": "aristocratgaminggr@gmail.com", + "address": "Str. Garii, Nr. 102", + "city": "Giurgiu", + "region": "Giurgiu", + "country": "Romania", + "zipcode": null + }, + "items": [ + { + "id": "279", + "type": "product", + "sku": "30006ozLavazza", + "ean": "", + "name": "Pahar carton 6oz Lavazza RLP bax 3000buc", + "price": "329.4", + "baseprice": "360", + "vat": "21", + "quantity": "1.00" + } + ], + "billing": { + "company": { + "name": "ARISTOCRAT GAMING SRL", + "code": "32128076", + "registrationNo": "J27/487/2013", + "bank": "", + "iban": "" + }, + "firstname": "Alexandra", + "lastname": "TONE", + "phone": "0763571486", + "email": "aristocratgaminggr@gmail.com", + "address": "Aleea Revolutiei, Spatiul Comercial Nr.32", + "city": "Roman", + "region": "Neamt", + "country": "Romania", + "customerId": "12283" + }, + "discounts": [ + ], + "updated": "2025-08-27 16:19:29" + } + }, + "shippingAsProduct": false +} \ No newline at end of file diff --git a/products-example.json b/products-example.json new file mode 100644 index 0000000..be0186e --- /dev/null +++ b/products-example.json @@ -0,0 +1,137 @@ +{ + "total": "Numar total de rezultate", + "page": "Pagina curenta", + "pages": "Numar total de pagini", + "products": { + "id": "ID intern al produsului", + "sku": "SKU", + "name": "Denumire", + "description": "Descriere", + "short_description": "Descriere scurta", + "url": "URL", + "canonical_url": "URL canonic", + "brand": "Marca produs", + "categories": { + "Branch 1": [ + { + "id": "ID-ul categoriei", + "name": "Denumirea categoriei" + }, + { + "id": "ID-ul categoriei", + "name": "Denumirea categoriei" + } + ], + "Branch 2": [ + { + "id": "ID-ul categoriei", + "name": "Denumirea categoriei" + }, + { + "id": "ID-ul categoriei", + "name": "Denumirea categoriei" + } + ], + "...": [ + "..." + ] + }, + "weight": "Greutatea produsului (kg)", + "enabled": "Activ (0/1)", + "parent": "ID produs parinte", + "parent_sku": "SKU produs parinte", + "group_key": "Codul de grupare al variantelor de produs", + "stockManagement": "Gestioneaza automat stocul produsului", + "stock": "Stoc cantitativ", + "stockStatus": "Status stoc", + "base_price": "Pret de baza al produsului", + "price": "Pret de vanzare al produsului (dupa aplicarea adaosurilor si reducerilor)", + "vat_included": "Pret include TVA (0/1)", + "vat": "Valoare TVA", + "currency": "Moneda", + "ecotax": "Valoare taxa verde", + "um": "Unitate de masura", + "html_title": "Titlu html", + "html_description": "Descrierere html", + "customSearchKeys": "Cuvinte cheie folosite la cautarea SEO", + "feedDescription": "Descriere pentru feed-uri", + "allowOrdersWhenOutOfStock": "Produsul se aduce la comanda", + "attributes": [ + { + "id": "ID atribut", + "type": "Tip atribut: dropdown, textinput, textarea", + "name": "Denumire atribut", + "value": "Optiune" + }, + { + "id": "ID atribut", + "type": "Tip atribut multipleselect (accepta valori multiple)", + "name": "Denumire atribut", + "value": [ + "Optiune1", + "Optiune2", + "..." + ] + } + ], + "images": [ + "Imagine 1 (principala)", + "Imagine 2", + "..." + ], + "variations": [ + { + "id": "ID intern al produsului", + "sku": "SKU", + "base_price": "Pret de baza al produsului", + "price": "Pret de vanzare al produsului (dupa aplicarea adaosurilor si reducerilor)", + "stock": "Stoc cantitativ", + "stockStatus": "Status stoc", + "stockManagement": "Gestioneaza automat stocul produsului", + "versionAttributes": { + "id Attribut": { + "name": "Denumire atribut", + "value": "Valoare atribut" + } + } + }, + { + "id": "ID intern al produsului", + "sku": "SKU", + "base_price": "Pret de baza al produsului", + "price": "Pret de vanzare al produsului (dupa aplicarea adaosurilor si reducerilor)", + "stock": "Stoc cantitativ", + "stockStatus": "Status stoc", + "stockManagement": "Gestioneaza automat stocul produsului", + "versionAttributes": { + "id Attribut": { + "id": "ID atribut", + "name": "Denumire atribut", + "value": "Valoare atribut" + } + } + } + ], + "ean": "EAN", + "videos": [ + "URL video" + ], + "files": [ + "URL fisiere" + ], + "updated": "Ultima modificare", + "created": "Data crearii", + "delivery_time": "Timp de livrare", + "delivery_time_type": "Tip timp de livrare", + "bundleItems": [ + { + "sku": "SKU componenta", + "quantity": "Cantitate" + }, + { + "sku": "SKU componenta", + "quantity": "Cantitate" + } + ] + } +} \ No newline at end of file diff --git a/regex.prg b/regex.prg new file mode 100644 index 0000000..272ca2e --- /dev/null +++ b/regex.prg @@ -0,0 +1,268 @@ +*!* CLEAR +*!* ?strtranx([ana are 1234567890.1234 lei], [\s\d+\.\d\s], [=TRANSFORM($1, "999 999 999 999.99")]) +*?strtranx([ana are <<1234567890.1234>> lei], [<<], [=TRANSFORM($1, "AA")]) +*!* RETURN +CLEAR + + +*-- http://www.cornerstonenw.com/article_id_parsing3.htm +SET STEP ON + +lcSourceString = [ana are mere 123,345 678 ad] +LOCAL laItems[10] + +lnResults = GetRegExpAll(lcSourceString, '\d+', @laItems) + +SET STEP ON +RETURN +strTest = [ab cd2""$$£] +?strTest +?StripNonAscii(strTest) + +*-- replace non a-z09 with "" case-insensitive +? strtranx([Ab ra /ca\d&abr'a],"[^a-z0-9]",[],1,,1) +RETURN + +*-- count words +? OccursRegExp("\b(\w+)\b", [the then quick quick brown fox fox]) +&& prints 7 + +*-- count repeatedwords +? OccursRegExp("\b(\w+)\s\1\b", [the then quick quick brown fox fox]) +&& prints 2 + + +*-- replace first and second lower-case "a" +? strtranx([Abracadabra],[a],[*],1,2) +&& prints Abr*c*dabra + +*-- replace first and second "a" case-insensitive +? strtranx([Abracadabra],[a],[*],1,2,1) +&& prints *br*cadabra + +*-- locate the replacement targets +? strtranx([Abracadabra],[^a|a$],[*],1,2,0) +&& Abracadabr* +? strtranx([Abracadabra],[^a|a$],[*],1,2,1) +&& *bracadabr* + + +lcText = "The cost, is $123,345.75. " +*-- convert the commas +lcText = strtranx( m.lcText, "(\d{1,3})\,(\d{1,}) ","$1 $2" ) + +*-- convert the decimals +? strtranx( m.lcText, "(\d{1,3})\.(\d{1,})", "$1,$2" ) + +** prints "The cost, is $123 345,75." + +*-- add 1 to all digits +? strtranx( [ABC123], "(\d)", [=TRANSFORM(VAL($1)+1)] ) +** prints "ABC234" + +*-- convert all dates to long format +? strtranx( [the date is: 7/18/2004 ] , [(\d{1,2}/\d{1,2}/\d{4})], [=TRANSFORM(CTOD($1),"@YL")]) +** prints "the date is: Sunday, July 18, 2004" + + +*---------------------------------------------------------- +FUNCTION StrtranRegExp( tcSourceString, tcPattern, tcReplace ) + LOCAL loRE + loRE = CREATEOBJECT("vbscript.regexp") + WITH loRE + .PATTERN = tcPattern + .GLOBAL = .T. + .multiline = .T. + RETURN .REPLACE( tcSourceString , tcReplace ) + ENDWITH +ENDFUNC + +*---------------------------------------------------------- +FUNCTION OccursRegExp(tcPattern, tcText) + LOCAL loRE, loMatches, lnResult + loRE = CREATEOBJECT("vbscript.regexp") + WITH loRE + .PATTERN = m.tcPattern + .GLOBAL = .T. + .multiline = .T. + loMatches = loRE.Execute( m.tcText ) + lnResult = loMatches.COUNT + loMatches = NULL + ENDWITH + RETURN m.lnResult +ENDFUNC + + + +*---------------------------------------------------------- +FUNCTION strtranx(tcSearched, ; + tcSearchFor, ; + tcReplacement, ; + tnStart, tnNumber, ; + tnFlag ) + + *-- the final version of the UDF + LOCAL loRE, lcText, lnShift, lcCommand,; + loMatch, loMatches, lnI, lnK, lcSubMatch,; + llevaluate, lcMatchDelim, lcReplaceText, lcReplacement,; + lnStart, lnNumber, loCol, lcKey + + IF EMPTY(NVL(tcSearched, '')) + RETURN NVL(tcSearched, '') + ENDIF + + loRE = CREATEOBJECT("vbscript.regexp") + + WITH loRE + .PATTERN = m.tcSearchFor + .GLOBAL = .T. + .multiline = .T. + .ignorecase = IIF(VARTYPE(m.tnFlag)=[N],m.tnFlag = 1,.F.) + ENDWITH + + lcReplacement = m.tcReplacement + + *--- are we evaluating? + IF m.lcReplacement = [=] + llevaluate = .T. + lcReplacement = SUBSTR( m.lcReplacement, 2 ) + ENDIF + + IF VARTYPE( m.tnStart )=[N] + lnStart = m.tnStart + ELSE + lnStart = 1 + ENDIF + + IF VARTYPE( m.tnNumber) =[N] + lnNumber = m.tnNumber + ELSE + lnNumber = -1 + ENDIF + + IF m.lnStart>1 OR m.lnNumber#-1 OR m.llevaluate + + lcText = m.tcSearched + lnShift = 1 + loMatches = loRE.execute( m.lcText ) + loCol = CREATEOBJECT([collection]) + lnNumber = IIF( lnNumber=-1,loMatches.COUNT,MIN(lnNumber,loMatches.COUNT)) + + FOR lnK = m.lnStart TO m.lnNumber + loMatch = loMatches.ITEM(m.lnK-1) && zero based + lcCommand = m.lcReplacement + FOR lnI= 1 TO loMatch.submatches.COUNT + lcSubMatch = loMatch.submatches(m.lnI-1) && zero based + IF m.llevaluate + * "escape" the string we are about to use in an evaluation. + * it is important to escape due to possible delim chars (like ", ' etc) + * malicious content, or VFP line-length violations. + lcKey = ALLTRIM(TRANSFORM(m.lnK)+[_]+TRANSFORM(m.lnI)) + loCol.ADD( m.lcSubMatch, m.lcKey ) + lcSubMatch = [loCol.item(']+m.lcKey+[')] + ENDIF + lcCommand = STRTRAN( m.lcCommand, "$" + ALLTRIM( STR( m.lnI ) ) , m.lcSubMatch) + ENDFOR + + IF m.llevaluate + TRY + lcReplaceText = EVALUATE( m.lcCommand ) + CATCH TO loErr + lcReplaceText="[[ERROR #"+TRANSFORM(loErr.ERRORNO)+[ ]+loErr.MESSAGE+"]]" + ENDTRY + ELSE + lcReplaceText = m.lcCommand + ENDIF + lcText = STUFF( m.lcText, loMatch.FirstIndex + m.lnShift, m.loMatch.LENGTH, m.lcReplaceText ) + lnShift = m.lnShift + LEN( m.lcReplaceText ) - m.loMatch.LENGTH + ENDFOR + ELSE + lcText = loRE.REPLACE( m.tcSearched, m.tcReplacement ) + ENDIF + RETURN m.lcText +ENDFUNC + +*===================== +FUNCTION StripNonAscii + LPARAMETERS tcSourceString, tcReplaceString + +TEXT TO lcPattern NOSHOW +[^A-Za-z 0-9 \.,\?'""!@#\$%\^&\*\(\)-_=\+;:<>\/\\\|\}\{\[\]`~] +ENDTEXT + lcReplace = IIF(TYPE('tcReplaceString') <> 'C', "", tcReplaceString) + lcReturn = strtranx( m.tcSourceString, m.lcPattern, m.lcReplace,1,,1) + + RETURN m.lcReturn +ENDFUNC && StripNonAscii + +*===================== +* Intoarce un text care se potriveste cu pattern-ul +* Ex. Localitatea din textul: STRADA NR LOCALITATE +*===================== +FUNCTION GetRegExp + LPARAMETERS tcSourceString, tcPattern, tnOccurence + + * tcSourceString: Bld. Stefan cel Mare 14 Tirgu Neamt + * tcPattern: [A-Za-z\s]+$ = (caracter sau spatiu) de cel putin o data la sfarsitul liniei = Tirgu Neamt + * tcPattern: \d+[A-Za-z\s]+$ = oricate cifre (caracter sau spatiu) de cel putin o data la sfarsitul liniei = 14 Tirgu Neamt + + LOCAL loRE, loMatches, lcResult, lnOccurence + lcResult = '' + lnOccurence = IIF(!EMPTY(m.tnOccurence) and TYPE('tnOccurence') = 'N', m.tnOccurence, 1) + + loRE = CREATEOBJECT("vbscript.regexp") + WITH loRE + .PATTERN = m.tcPattern + .GLOBAL = .T. + .multiline = .T. + loMatches = loRE.Execute( m.tcSourceString) + IF loMatches.COUNT >= m.lnOccurence + lcResult = loMatches.Item(m.lnOccurence - 1).Value + ENDIF + loMatches = NULL + ENDWITH + + RETURN m.lcResult +ENDFUNC && GetRegExp + +*===================== +* Intoarce numarul potrivirilor si un parametru OUT array sau lista de numere facturi separate prin "," +* Ex. Toate numerele dintr-un text lnMatches = GetRegExpAll(lcSourceString, '\d+', @loMatches) +*===================== +FUNCTION GetRegExpAll + LPARAMETERS tcSourceString, tcPattern, taItems + + * tcSourceString: Bld. Stefan cel Mare 14 Tirgu Neamt + * tcPattern: [A-Za-z\s]+$ = (caracter sau spatiu) de cel putin o data la sfarsitul liniei = Tirgu Neamt + * tcPattern: \d+[A-Za-z\s]+$ = oricate cifre (caracter sau spatiu) de cel putin o data la sfarsitul liniei = 14 Tirgu Neamt + * taItems "A">taItems : array cu rezultatele (OUT) taItems[1..Result] sau taItems "C" lista facturi separate prin virgula + LOCAL loRE, loMatches, lnResults, lnItem + IF TYPE('taItems') = "A" + EXTERNAL ARRAY taItems + ELSE + taItems = "" + ENDIF + lnResult = 0 + + loRE = CREATEOBJECT("vbscript.regexp") + WITH loRE + .PATTERN = m.tcPattern + .GLOBAL = .T. + .multiline = .T. + loMatches = loRE.Execute( m.tcSourceString) + lnResults = loMatches.COUNT + IF TYPE('taItems') = "A" + DIMENSION taItems[m.lnResult] + FOR lnItem = 1 TO m.lnResult + taItems[m.lnItem] = loMatches.Item(m.lnItem-1).Value + ENDFOR + ELSE + FOR lnItem = 1 TO m.lnResults + taItems = taItems + IIF(m.lnItem > 1, ",", "") + loMatches.Item(m.lnItem-1).Value + ENDFOR + ENDIF + loMatches = NULL + ENDWITH + + RETURN m.lnResults +ENDFUNC && GetRegExp \ No newline at end of file diff --git a/run-gomag.bat b/run-gomag.bat new file mode 100644 index 0000000..b5b4ec8 --- /dev/null +++ b/run-gomag.bat @@ -0,0 +1,4 @@ +@echo off +cd /d "%~dp0" +"C:\Program Files (x86)\Microsoft Visual FoxPro 9\vfp9.exe" -T "%~dp0gomag-vending.prg" +pause \ No newline at end of file diff --git a/settings.ini b/settings.ini new file mode 100644 index 0000000..9bd7890 --- /dev/null +++ b/settings.ini @@ -0,0 +1,17 @@ +[API] +ApiBaseUrl=https://api.gomag.ro/api/v1/product/read/json?enabled=1 +OrderApiUrl=https://api.gomag.ro/api/v1/order/read/json +ApiKey=4c5e46df8f6c4f054fe2787de7a13d4a +ApiShop=https://www.coffeepoint.ro +UserAgent=Mozilla/5.0 +ContentType=application/json + +[PAGINATION] +Limit=100 + +[OPTIONS] +GetProducts=1 +GetOrders=1 + +[FILTERS] +OrderDaysBack=7 \ No newline at end of file diff --git a/utils.prg b/utils.prg new file mode 100644 index 0000000..5895dcb --- /dev/null +++ b/utils.prg @@ -0,0 +1,264 @@ +*-- utils.prg - Utilitare pentru GoMag API +*-- Functii pentru citirea/scrierea fisierelor INI si alte utilitare +*-- Autor: Claude AI +*-- Data: 27.08.2025 + +*-- Functie pentru citirea fisierelor INI private +*-- Returneaza valoarea din sectiunea si intrarea specificata sau blank daca nu e gasita +FUNCTION ReadPini +PARAMETERS cSection, cEntry, cINIFile +LOCAL cDefault, cRetVal, nRetLen + +cDefault = "" +cRetVal = SPACE(255) +nRetLen = LEN(cRetVal) + +DECLARE INTEGER GetPrivateProfileString IN WIN32API ; + STRING cSection, ; + STRING cEntry, ; + STRING cDefault, ; + STRING @cRetVal, ; + INTEGER nRetLen, ; + STRING cINIFile + +nRetLen = GetPrivateProfileString(cSection, ; + cEntry, ; + cDefault, ; + @cRetVal, ; + nRetLen, ; + cINIFile) + +RETURN LEFT(cRetVal, nRetLen) +ENDFUNC + +*-- Functie pentru scrierea in fisierele INI private +*-- Returneaza .T. daca e successful, .F. daca nu +FUNCTION WritePini +PARAMETERS cSection, cEntry, cValue, cINIFile +LOCAL nRetVal + +DECLARE INTEGER WritePrivateProfileString IN WIN32API ; + STRING cSection, ; + STRING cEntry, ; + STRING cValue, ; + STRING cINIFile + +nRetVal = WritePrivateProfileString(cSection, ; + cEntry, ; + cValue, ; + cINIFile) + +RETURN nRetVal = 1 +ENDFUNC + +*-- Functie pentru incarcarea tuturor setarilor din fisierul INI +FUNCTION LoadSettings +PARAMETERS cINIFile +LOCAL loSettings + +*-- Cream un obiect pentru toate setarile +loSettings = CREATEOBJECT("Empty") + +*-- Sectiunea API +ADDPROPERTY(loSettings, "ApiBaseUrl", ReadPini("API", "ApiBaseUrl", cINIFile)) +ADDPROPERTY(loSettings, "OrderApiUrl", ReadPini("API", "OrderApiUrl", cINIFile)) +ADDPROPERTY(loSettings, "ApiKey", ReadPini("API", "ApiKey", cINIFile)) +ADDPROPERTY(loSettings, "ApiShop", ReadPini("API", "ApiShop", cINIFile)) +ADDPROPERTY(loSettings, "UserAgent", ReadPini("API", "UserAgent", cINIFile)) +ADDPROPERTY(loSettings, "ContentType", ReadPini("API", "ContentType", cINIFile)) + +*-- Sectiunea PAGINATION +ADDPROPERTY(loSettings, "Limit", VAL(ReadPini("PAGINATION", "Limit", cINIFile))) + +*-- Sectiunea OPTIONS +ADDPROPERTY(loSettings, "GetProducts", ReadPini("OPTIONS", "GetProducts", cINIFile) = "1") +ADDPROPERTY(loSettings, "GetOrders", ReadPini("OPTIONS", "GetOrders", cINIFile) = "1") + +*-- Sectiunea FILTERS +ADDPROPERTY(loSettings, "OrderDaysBack", VAL(ReadPini("FILTERS", "OrderDaysBack", cINIFile))) + +RETURN loSettings +ENDFUNC + +*-- Test conectivitate internet +FUNCTION TestConnectivity +LOCAL loHttp, llResult + +llResult = .T. + +TRY + loHttp = CREATEOBJECT("WinHttp.WinHttpRequest.5.1") + loHttp.Open("GET", "https://www.google.com", .F.) + loHttp.SetTimeouts(5000, 5000, 5000, 5000) + loHttp.Send() + + IF loHttp.Status != 200 + llResult = .F. + ENDIF + +CATCH + llResult = .F. +ENDTRY + +loHttp = NULL +RETURN llResult +ENDFUNC + +*-- Functie pentru codificare URL +FUNCTION UrlEncode +PARAMETERS tcString + +LOCAL lcResult, lcChar, lnI + +lcResult = "" + +FOR lnI = 1 TO LEN(tcString) + lcChar = SUBSTR(tcString, lnI, 1) + + DO CASE + CASE ISALPHA(lcChar) OR ISDIGIT(lcChar) OR INLIST(lcChar, "-", "_", ".", "~") + lcResult = lcResult + lcChar + OTHERWISE + lcResult = lcResult + "%" + RIGHT("0" + TRANSFORM(ASC(lcChar), "@0"), 2) + ENDCASE +ENDFOR + +RETURN lcResult +ENDFUNC + +*-- Functie pentru verificarea existentei fisierului INI +FUNCTION CheckIniFile +PARAMETERS cINIFile +LOCAL llExists + +TRY + llExists = FILE(cINIFile) +CATCH + llExists = .F. +ENDTRY + +RETURN llExists +ENDFUNC + +*-- Functie pentru crearea unui fisier INI implicit cu setari de baza +FUNCTION CreateDefaultIni +PARAMETERS cINIFile +LOCAL llSuccess + +llSuccess = .T. + +TRY + *-- Sectiunea API + WritePini("API", "ApiBaseUrl", "https://api.gomag.ro/api/v1/product/read/json?enabled=1", cINIFile) + WritePini("API", "OrderApiUrl", "https://api.gomag.ro/api/v1/order/read/json", cINIFile) + WritePini("API", "ApiKey", "YOUR_API_KEY_HERE", cINIFile) + WritePini("API", "ApiShop", "https://yourstore.gomag.ro", cINIFile) + WritePini("API", "UserAgent", "Mozilla/5.0", cINIFile) + WritePini("API", "ContentType", "application/json", cINIFile) + + *-- Sectiunea PAGINATION + WritePini("PAGINATION", "Limit", "100", cINIFile) + + *-- Sectiunea OPTIONS + WritePini("OPTIONS", "GetProducts", "1", cINIFile) + WritePini("OPTIONS", "GetOrders", "1", cINIFile) + + *-- Sectiunea FILTERS + WritePini("FILTERS", "OrderDaysBack", "7", cINIFile) + +CATCH + llSuccess = .F. +ENDTRY + +RETURN llSuccess +ENDFUNC + +*-- Functie pentru initializarea logging-ului +FUNCTION InitLog +PARAMETERS cBaseName +LOCAL lcLogFile, lcStartTime, lcLogHeader, lcLogDir + +*-- Cream directorul log daca nu existe +lcLogDir = gcAppPath + "log" +IF !DIRECTORY(lcLogDir) + MKDIR (lcLogDir) +ENDIF + +*-- Generam numele fisierului log cu timestamp in directorul log +lcStartTime = DTOS(DATE()) + "_" + STRTRAN(TIME(), ":", "") +lcLogFile = lcLogDir + "\" + cBaseName + "_" + lcStartTime + ".log" + +*-- Header pentru log +lcLogHeader = "[" + TIME() + "] [START] === GoMag Sync Started ===" + CHR(13) + CHR(10) +lcLogHeader = lcLogHeader + "[" + TIME() + "] [INFO ] Date: " + DTOC(DATE()) + " | VFP: " + VERSION() + CHR(13) + CHR(10) + +*-- Cream fisierul log +STRTOFILE(lcLogHeader, lcLogFile) + +RETURN lcLogFile +ENDFUNC + +*-- Functie pentru logging cu nivel si timestamp +FUNCTION LogMessage +PARAMETERS cMessage, cLevel, cLogFile +LOCAL lcTimeStamp, lcLogEntry, lcExistingContent + +*-- Setam nivel implicit daca nu e specificat +IF EMPTY(cLevel) + cLevel = "INFO " +ELSE + *-- Formatam nivelul pentru a avea 5 caractere + cLevel = LEFT(cLevel + " ", 5) +ENDIF + +*-- Cream timestamp-ul +lcTimeStamp = TIME() + +*-- Formatam mesajul pentru log +lcLogEntry = "[" + lcTimeStamp + "] [" + cLevel + "] " + cMessage + CHR(13) + CHR(10) + +*-- Adaugam la fisierul existent +lcExistingContent = "" +IF FILE(cLogFile) + lcExistingContent = FILETOSTR(cLogFile) +ENDIF + +STRTOFILE(lcExistingContent + lcLogEntry, cLogFile) + +RETURN .T. +ENDFUNC + +*-- Functie pentru inchiderea logging-ului cu statistici +FUNCTION CloseLog +PARAMETERS nStartTime, nProductsCount, nOrdersCount, cLogFile +LOCAL lcEndTime, lcDuration, lcStatsEntry + +lcEndTime = TIME() +*-- Calculam durata in secunde +nDuration = SECONDS() - nStartTime + +*-- Formatam statisticile finale +lcStatsEntry = "[" + lcEndTime + "] [END ] === GoMag Sync Completed ===" + CHR(13) + CHR(10) +lcStatsEntry = lcStatsEntry + "[" + lcEndTime + "] [STATS] Duration: " + TRANSFORM(INT(nDuration)) + "s" + +IF nProductsCount > 0 + lcStatsEntry = lcStatsEntry + " | Products: " + TRANSFORM(nProductsCount) +ENDIF + +IF nOrdersCount > 0 + lcStatsEntry = lcStatsEntry + " | Orders: " + TRANSFORM(nOrdersCount) +ENDIF + +lcStatsEntry = lcStatsEntry + CHR(13) + CHR(10) + +*-- Adaugam la log +LOCAL lcExistingContent +lcExistingContent = "" +IF FILE(cLogFile) + lcExistingContent = FILETOSTR(cLogFile) +ENDIF + +STRTOFILE(lcExistingContent + lcStatsEntry, cLogFile) + +RETURN .T. +ENDFUNC \ No newline at end of file