Guide d'intégration pour les systèmes de vente avec IzyScan
Version du Document: 2.2
Dernière Mise à Jour: 16 Juillet 2025 à 15h57
Ce document fournit les spécifications techniques pour intégrer les systèmes de vente avec IzyScan Server. IzyScan est un système d'affichage des prix et de gestion des stocks qui nécessite une synchronisation avec les systèmes POS/vente existants.
Cette section vous montre comment créer et démarrer un serveur web simple sur le port 80 dans différents langages de programmation.
// server.php
<?php
// Route principale
if ($_SERVER['REQUEST_URI'] == '/') {
header('Content-Type: application/json');
echo json_encode(['status' => 'success', 'message' => 'Serveur en marche']);
exit;
}
// Autres routes retournent 404
http_response_code(404);
echo json_encode(['status' => 'error', 'message' => 'Route non trouvée']);
// Pour démarrer le serveur sur le port 80:
// sudo php -S 0.0.0.0:80 server.php
?>
// server.js
const http = require('http');
const server = http.createServer((req, res) => {
res.setHeader('Content-Type', 'application/json');
if (req.url === '/' && req.method === 'GET') {
res.statusCode = 200;
res.end(JSON.stringify({
status: 'success',
message: 'Serveur en marche'
}));
} else {
res.statusCode = 404;
res.end(JSON.stringify({
status: 'error',
message: 'Route non trouvée'
}));
}
});
const PORT = 80;
server.listen(PORT, () => {
console.log(`Serveur démarré sur http://localhost:${PORT}`);
});
// Pour démarrer: sudo node server.js
// Program.cs
using System;
using System.Net;
using System.Text;
using System.Text.Json;
class Program
{
static void Main()
{
HttpListener listener = new HttpListener();
listener.Prefixes.Add("http://*:80/");
listener.Start();
Console.WriteLine("Serveur démarré sur http://localhost:80");
while (true)
{
HttpListenerContext context = listener.GetContext();
HttpListenerResponse response = context.Response;
response.ContentType = "application/json";
string responseString;
if (context.Request.Url.AbsolutePath == "/")
{
responseString = JsonSerializer.Serialize(new {
status = "success",
message = "Serveur en marche"
});
response.StatusCode = 200;
}
else
{
responseString = JsonSerializer.Serialize(new {
status = "error",
message = "Route non trouvée"
});
response.StatusCode = 404;
}
byte[] buffer = Encoding.UTF8.GetBytes(responseString);
response.ContentLength64 = buffer.Length;
response.OutputStream.Write(buffer, 0, buffer.Length);
response.Close();
}
}
}
// Pour compiler et démarrer:
// dotnet run (en tant qu'administrateur)
// SimpleServer.java
import com.sun.net.httpserver.*;
import java.io.*;
import java.net.InetSocketAddress;
public class SimpleServer {
public static void main(String[] args) throws IOException {
HttpServer server = HttpServer.create(new InetSocketAddress(80), 0);
server.createContext("/", exchange -> {
String response;
int statusCode;
if (exchange.getRequestURI().getPath().equals("/")) {
response = "{\"status\":\"success\",\"message\":\"Serveur en marche\"}";
statusCode = 200;
} else {
response = "{\"status\":\"error\",\"message\":\"Route non trouvée\"}";
statusCode = 404;
}
exchange.getResponseHeaders().set("Content-Type", "application/json");
exchange.sendResponseHeaders(statusCode, response.length());
OutputStream os = exchange.getResponseBody();
os.write(response.getBytes());
os.close();
});
server.start();
System.out.println("Serveur démarré sur http://localhost:80");
}
}
// Pour compiler et démarrer:
// javac SimpleServer.java
// sudo java SimpleServer
# server.py
from http.server import HTTPServer, BaseHTTPRequestHandler
import json
class SimpleHandler(BaseHTTPRequestHandler):
def do_GET(self):
self.send_response(200 if self.path == '/' else 404)
self.send_header('Content-Type', 'application/json')
self.end_headers()
if self.path == '/':
response = {
'status': 'success',
'message': 'Serveur en marche'
}
else:
response = {
'status': 'error',
'message': 'Route non trouvée'
}
self.wfile.write(json.dumps(response).encode())
if __name__ == '__main__':
server = HTTPServer(('', 80), SimpleHandler)
print('Serveur démarré sur http://localhost:80')
server.serve_forever()
# Pour démarrer: sudo python server.py
program SimpleWebServer;
{$APPTYPE CONSOLE}
uses
System.SysUtils,
IdHTTPServer,
IdContext,
IdCustomHTTPServer;
type
TSimpleServer = class
procedure HandleRequest(AContext: TIdContext;
ARequestInfo: TIdHTTPRequestInfo;
AResponseInfo: TIdHTTPResponseInfo);
end;
procedure TSimpleServer.HandleRequest(AContext: TIdContext;
ARequestInfo: TIdHTTPRequestInfo; AResponseInfo: TIdHTTPResponseInfo);
begin
AResponseInfo.ContentType := 'application/json';
if ARequestInfo.Document = '/' then
begin
AResponseInfo.ResponseNo := 200;
AResponseInfo.ContentText := '{"status":"success","message":"Serveur en marche"}';
end
else
begin
AResponseInfo.ResponseNo := 404;
AResponseInfo.ContentText := '{"status":"error","message":"Route non trouvée"}';
end;
end;
var
Server: TIdHTTPServer;
Handler: TSimpleServer;
begin
Server := TIdHTTPServer.Create(nil);
Handler := TSimpleServer.Create;
try
Server.DefaultPort := 80;
Server.OnCommandGet := Handler.HandleRequest;
Server.Active := True;
WriteLn('Serveur démarré sur http://localhost:80');
WriteLn('Appuyez sur Entrée pour arrêter...');
ReadLn;
finally
Server.Free;
Handler.Free;
end;
end.
// Procédure principale
PROCEDURE DémarrerServeur()
// Créer un serveur web sur le port 80
MonServeur est un httpServeur
MonServeur..Port = 80
MonServeur..RacineDocumentaire = fRepExe()
// Ajouter une route pour "/"
MonServeur..AjouteRoute("/", ProcédureAccueil)
// Démarrer le serveur
SI MonServeur..Démarre() ALORS
Info("Serveur démarré sur http://localhost:80")
SINON
Erreur("Impossible de démarrer le serveur")
FIN
FIN
// Procédure pour gérer la route principale
PROCEDURE ProcédureAccueil(Requête est un httpRequête, Réponse est un httpRéponse)
Réponse..TypeContenu = "application/json"
Réponse..CodeStatut = 200
vRéponse est un Variant
vRéponse.status = "success"
vRéponse.message = "Serveur en marche"
Réponse..Contenu = VariantVersJSON(vRéponse)
FIN
// Appel de la procédure
DémarrerServeur()
sudo pour démarrer le serveurhttp://localhost/IzyScan supporte trois méthodes d'authentification (configurables par installation):
Si les champs login et mot de passe sont vides, les requêtes sont envoyées sans en-têtes d'authentification.
Si seul le champ mot de passe est configuré, il est traité comme un token d'accès:
Authorization: Bearer votre_token_acces_ici
Si le login et le mot de passe sont fournis:
Authorization: Basic base64(nom_utilisateur:mot_de_passe)
Objectif: Tester la connectivité avec le serveur
Ce point de terminaison permet à IzyScan de vérifier que le serveur est accessible et répond correctement.
| Statut | Description |
|---|---|
| 200 OK | Le corps de la réponse peut être n'importe quoi (non analysé par le client) |
// PHP - GET /api/ping
public function ping() {
return response()->json([
'status' => 'ok',
'message' => 'pong'
], 200);
}
// Dans routes/api.php
Route::get('/api/ping', 'ApiController@ping');
// Node.js - GET /api/ping
app.get('/api/ping', (req, res) => {
res.status(200).json({
status: 'ok',
message: 'pong'
});
});
// C# - GET /api/ping
[HttpGet("ping")]
public IActionResult Ping()
{
return Ok(new
{
status = "ok",
message = "pong"
});
}
// Java - GET /api/ping
@GetMapping("/api/ping")
public ResponseEntity> ping() {
return ResponseEntity.ok(Map.of(
"status", "ok",
"message", "pong"
));
}
# Python - GET /api/ping
@app.route('/api/ping', methods=['GET'])
def ping():
return jsonify({
'status': 'ok',
'message': 'pong'
}), 200
// Delphi - GET /api/ping
procedure TSimpleServer.HandlePing(AContext: TIdContext;
ARequestInfo: TIdHTTPRequestInfo; AResponseInfo: TIdHTTPResponseInfo);
begin
AResponseInfo.ContentType := 'application/json';
AResponseInfo.ResponseNo := 200;
AResponseInfo.ContentText := '{"status":"ok","message":"pong"}';
end;
// WinDev - GET /api/ping
PROCEDURE ProcédurePing(Requête est un httpRequête, Réponse est un httpRéponse)
Réponse..TypeContenu = "application/json"
Réponse..CodeStatut = 200
vRéponse est un Variant
vRéponse.status = "ok"
vRéponse.message = "pong"
Réponse..Contenu = VariantVersJSON(vRéponse)
FIN
Tous les points de terminaison doivent accepter du contenu JSON et retourner des réponses JSON.
Content-Type: application/json et Accept: application/json
Objectif: Récupérer les produits pour la synchronisation avec IzyScan
| Paramètre | Type | Obligatoire | Description |
|---|---|---|---|
updated_since |
string (ISO 8601) | Optionnel | Pour la synchronisation incrémentale (ex: 2024-01-15 10:30:00). Retourne uniquement les produits créés ou modifiés après cette date. |
limit |
integer | Optionnel | Nombre maximum de produits par requête (le client demande 2000) |
offset |
integer | Optionnel | Nombre de produits à ignorer (pour la pagination, défaut: 0) |
GET /api/products?updated_since=2024-01-15 10:30:00&limit=2000&offset=0
{
"data": [
{
"id": 123,
"name": "Nom du Produit",
"barcode": "1234567890123",
"price": 29.99,
"promo_price": 25.99,
"server_id": 123
}
],
"pagination": {
"total": 15000,
"offset": 0,
"limit": 2000,
"has_more": true
}
}
| Champ | Type | Obligatoire | Description |
|---|---|---|---|
id |
string/number | Obligatoire | Identifiant unique du produit (peut être identique au barcode) |
name |
string | Obligatoire | Nom d'affichage du produit |
barcode |
string | Obligatoire | Code-barres du produit (EAN-13, UPC, etc.). Les produits sans code-barres sont ignorés. |
price |
number | Obligatoire | Prix de vente actuel |
promo_price |
number | Optionnel | Prix promotionnel (si applicable) |
server_id |
string/number | Optionnel | ID du produit côté serveur (utilisé pour les mises à jour de prix) |
| Champ | Type | Obligatoire | Description |
|---|---|---|---|
total |
integer | Recommandé | Nombre total de produits correspondant à la requête |
offset |
integer | Recommandé | Décalage actuel |
limit |
integer | Recommandé | Limite demandée |
has_more |
boolean | Recommandé | true si plus de produits disponibles, false sinon |
has_more soit false// PHP - GET /api/products
public function getProducts(Request $request) {
$updatedSince = $request->query('updated_since');
$limit = min($request->query('limit', 2000), 2000);
$offset = $request->query('offset', 0);
$query = Product::where('barcode', '!=', null)
->where('barcode', '!=', '');
if ($updatedSince) {
$query->where('updated_at', '>=', $updatedSince);
}
$total = $query->count();
$products = $query->offset($offset)->limit($limit)->get();
return response()->json([
'data' => $products->map(function($product) {
return [
'id' => $product->id,
'name' => $product->name,
'barcode' => $product->barcode,
'price' => (float) $product->price,
'promo_price' => $product->promo_price ? (float) $product->promo_price : null,
'server_id' => $product->id,
];
}),
'pagination' => [
'total' => $total,
'offset' => $offset,
'limit' => $limit,
'has_more' => ($offset + $limit) < $total,
]
]);
}
// Node.js - GET /api/products
app.get('/api/products', async (req, res) => {
const { updated_since, limit = 2000, offset = 0 } = req.query;
let query = db.products.find({
barcode: { $ne: null, $ne: '' }
});
if (updated_since) {
query = query.where('updated_at').gte(new Date(updated_since));
}
const total = await db.products.countDocuments({
barcode: { $ne: null, $ne: '' }
});
const products = await query
.skip(parseInt(offset))
.limit(Math.min(parseInt(limit), 2000))
.exec();
res.json({
data: products.map(product => ({
id: product._id,
name: product.name,
barcode: product.barcode,
price: parseFloat(product.price),
promo_price: product.promo_price ? parseFloat(product.promo_price) : null,
server_id: product._id,
})),
pagination: {
total,
offset: parseInt(offset),
limit: parseInt(limit),
has_more: (parseInt(offset) + parseInt(limit)) < total,
}
});
});
// C# - GET /api/products
[HttpGet("products")]
public async Task GetProducts(
[FromQuery] DateTime? updatedSince,
[FromQuery] int limit = 2000,
[FromQuery] int offset = 0)
{
limit = Math.Min(limit, 2000);
var query = _context.Products
.Where(p => p.Barcode != null && p.Barcode != "");
if (updatedSince.HasValue)
{
query = query.Where(p => p.UpdatedAt >= updatedSince.Value);
}
var total = await query.CountAsync();
var products = await query
.Skip(offset)
.Take(limit)
.Select(p => new
{
id = p.Id,
name = p.Name,
barcode = p.Barcode,
price = p.Price,
promo_price = (decimal?)p.PromoPrice,
server_id = p.Id
})
.ToListAsync();
return Ok(new
{
data = products,
pagination = new
{
total,
offset,
limit,
has_more = (offset + limit) < total
}
});
}
// Java - GET /api/products
@GetMapping("/api/products")
public ResponseEntity> getProducts(
@RequestParam(required = false) String updatedSince,
@RequestParam(defaultValue = "2000") int limit,
@RequestParam(defaultValue = "0") int offset) {
limit = Math.min(limit, 2000);
Specification spec = (root, query, cb) ->
cb.and(
cb.isNotNull(root.get("barcode")),
cb.notEqual(root.get("barcode"), "")
);
if (updatedSince != null) {
LocalDateTime date = LocalDateTime.parse(updatedSince);
spec = spec.and((root, query, cb) ->
cb.greaterThanOrEqualTo(root.get("updatedAt"), date));
}
long total = productRepository.count(spec);
Pageable pageable = PageRequest.of(offset / limit, limit);
Page page = productRepository.findAll(spec, pageable);
List
# Python - GET /api/products
from flask import Flask, request, jsonify
from datetime import datetime
@app.route('/api/products', methods=['GET'])
def get_products():
updated_since = request.args.get('updated_since')
limit = min(int(request.args.get('limit', 2000)), 2000)
offset = int(request.args.get('offset', 0))
query = Product.query.filter(
Product.barcode.isnot(None),
Product.barcode != ''
)
if updated_since:
date = datetime.fromisoformat(updated_since.replace('Z', '+00:00'))
query = query.filter(Product.updated_at >= date)
total = query.count()
products = query.offset(offset).limit(limit).all()
return jsonify({
'data': [{
'id': p.id,
'name': p.name,
'barcode': p.barcode,
'price': float(p.price),
'promo_price': float(p.promo_price) if p.promo_price else None,
'server_id': p.id
} for p in products],
'pagination': {
'total': total,
'offset': offset,
'limit': limit,
'has_more': (offset + limit) < total
}
})
// Delphi - GET /api/products
procedure GetProducts(const UpdatedSince: string; Limit, Offset: Integer);
var
HttpClient: TNetHTTPClient;
Response: IHTTPResponse;
URL: string;
JSONResponse: TJSONObject;
Products: TJSONArray;
begin
HttpClient := TNetHTTPClient.Create(nil);
try
URL := Format('https://api.example.com/api/products?limit=%d&offset=%d',
[Limit, Offset]);
if UpdatedSince <> '' then
URL := URL + '&updated_since=' + UpdatedSince;
HttpClient.Accept := 'application/json';
Response := HttpClient.Get(URL);
if Response.StatusCode = 200 then
begin
JSONResponse := TJSONObject.ParseJSONValue(Response.ContentAsString) as TJSONObject;
try
if JSONResponse.GetValue('status').Value = 'success' then
begin
Products := JSONResponse.GetValue('data');
// Traiter les produits...
end;
finally
JSONResponse.Free;
end;
end;
finally
HttpClient.Free;
end;
end;
// WinDev - GET /api/products
PROCEDURE RécupérerProduits(dtDepuisMiseAJour est un DateHeure = "", nLimite est un entier = 2000, nOffset est un entier = 0)
tabProduits est un tableau de STProduct
sURL = gsURLAPI + "/api/products?"
sURL += "limit=" + nLimite
sURL += "&offset=" + nOffset
SI dtDepuisMiseAJour <> "" ALORS
sURL += "&updated_since=" + DateHeureVersISO8601(dtDepuisMiseAJour)
FIN
requeteHTTP est un httpRequête
requeteHTTP.URL = sURL
requeteHTTP.Méthode = httpGet
tabEntetes est un tableau associatif de chaînes = ObtenirEntetes()
POUR TOUT sValeur, sCle DE tabEntetes
requeteHTTP.Entête[sCle] = sValeur
FIN
réponseHTTP est un httpRéponse = HTTPEnvoie(requeteHTTP)
SI réponseHTTP.CodeEtat = 200 ALORS
vJSON est un Variant = JSONVersVariant(réponseHTTP.Contenu)
POUR TOUT vProduit DE vJSON.data
stProduit est un STProduct
stProduit.id = vProduit.id
stProduit.name = vProduit.name
stProduit.barcode = vProduit.barcode
stProduit.price = vProduit.price
stProduit.promo_price = vProduit.promo_price
stProduit.server_id = vProduit.server_id
Ajoute(tabProduits, stProduit)
FIN
FIN
RENVOYER tabProduits
FIN
Objectif: Mettre à jour le prix d'un produit depuis IzyScan
barcode: Code-barres du produit{
"barcode": "1234567890123",
"price": 32.99,
"server_id": 123
}
{
"status": "success",
"message": "Prix mis à jour avec succès",
"data": {
"server_id": 123,
"barcode": "1234567890123",
"old_price": 29.99,
"new_price": 32.99,
"updated_at": "2025-07-16T15:30:00Z"
}
}
// PHP - PUT ou POST /api/products/{barcode}/price
public function updatePrice(Request $request, $barcode) {
$validated = $request->validate([
'price' => 'required|numeric|min:0',
'server_id' => 'required|integer'
]);
$product = Product::where('barcode', $barcode)->first();
if (!$product) {
return response()->json([
'status' => 'error',
'error_code' => 'PRODUCT_NOT_FOUND',
'message' => 'Produit non trouvé'
], 404);
}
$oldPrice = $product->price;
$product->price = $validated['price'];
$product->save();
return response()->json([
'status' => 'success',
'message' => 'Prix mis à jour avec succès',
'data' => [
'server_id' => $product->id,
'barcode' => $product->barcode,
'old_price' => (float) $oldPrice,
'new_price' => (float) $product->price,
'updated_at' => $product->updated_at->toISOString(),
]
]);
}
// Dans routes/api.php
Route::match(['PUT', 'POST'], '/api/products/{barcode}/price', 'ProductController@updatePrice');
// Node.js - PUT ou POST /api/products/:barcode/price
app.put('/api/products/:barcode/price', updatePrice);
app.post('/api/products/:barcode/price', updatePrice);
async function updatePrice(req, res) {
const { barcode } = req.params;
const { price, server_id } = req.body;
if (!price || price < 0) {
return res.status(400).json({
status: 'error',
error_code: 'INVALID_PRICE',
message: 'Le prix doit être un nombre positif'
});
}
const product = await db.products.findOne({ barcode });
if (!product) {
return res.status(404).json({
status: 'error',
error_code: 'PRODUCT_NOT_FOUND',
message: 'Produit non trouvé'
});
}
const oldPrice = product.price;
product.price = price;
product.updated_at = new Date();
await product.save();
res.json({
status: 'success',
message: 'Prix mis à jour avec succès',
data: {
server_id: product._id,
barcode: product.barcode,
old_price: parseFloat(oldPrice),
new_price: parseFloat(price),
updated_at: product.updated_at.toISOString(),
}
});
}
// C# - PUT ou POST /api/products/{barcode}/price
[HttpPut("products/{barcode}/price")]
[HttpPost("products/{barcode}/price")]
public async Task UpdatePrice(
string barcode,
[FromBody] PriceUpdateRequest request)
{
if (request.Price < 0)
{
return BadRequest(new ErrorResponse
{
ErrorCode = "INVALID_PRICE",
Message = "Le prix doit être un nombre positif"
});
}
var product = await _context.Products
.FirstOrDefaultAsync(p => p.Barcode == barcode);
if (product == null)
{
return NotFound(new ErrorResponse
{
ErrorCode = "PRODUCT_NOT_FOUND",
Message = "Produit non trouvé"
});
}
var oldPrice = product.Price;
product.Price = request.Price;
product.UpdatedAt = DateTime.UtcNow;
await _context.SaveChangesAsync();
return Ok(new
{
status = "success",
message = "Prix mis à jour avec succès",
data = new
{
server_id = product.Id,
barcode = product.Barcode,
old_price = oldPrice,
new_price = product.Price,
updated_at = product.UpdatedAt.ToString("yyyy-MM-dd'T'HH:mm:ss'Z'")
}
});
}
// Java - PUT ou POST /api/products/{barcode}/price
@PutMapping("/api/products/{barcode}/price")
@PostMapping("/api/products/{barcode}/price")
public ResponseEntity> updatePrice(
@PathVariable String barcode,
@RequestBody PriceUpdateRequest request) {
if (request.getPrice().compareTo(BigDecimal.ZERO) < 0) {
return ResponseEntity.badRequest().body(Map.of(
"status", "error",
"error_code", "INVALID_PRICE",
"message", "Le prix doit être un nombre positif"
));
}
Optional productOpt = productRepository.findByBarcode(barcode);
if (productOpt.isEmpty()) {
return ResponseEntity.status(404).body(Map.of(
"status", "error",
"error_code", "PRODUCT_NOT_FOUND",
"message", "Produit non trouvé"
));
}
Product product = productOpt.get();
BigDecimal oldPrice = product.getPrice();
product.setPrice(request.getPrice());
product.setUpdatedAt(LocalDateTime.now());
productRepository.save(product);
return ResponseEntity.ok(Map.of(
"status", "success",
"message", "Prix mis à jour avec succès",
"data", Map.of(
"server_id", product.getId(),
"barcode", product.getBarcode(),
"old_price", oldPrice,
"new_price", product.getPrice(),
"updated_at", product.getUpdatedAt().toString()
)
));
}
# Python - PUT ou POST /api/products/{barcode}/price
@app.route('/api/products//price', methods=['PUT', 'POST'])
def update_price(barcode):
data = request.get_json()
if 'price' not in data or data['price'] < 0:
return jsonify({
'status': 'error',
'error_code': 'INVALID_PRICE',
'message': 'Le prix doit être un nombre positif'
}), 400
product = Product.query.filter_by(barcode=barcode).first()
if not product:
return jsonify({
'status': 'error',
'error_code': 'PRODUCT_NOT_FOUND',
'message': 'Produit non trouvé'
}), 404
old_price = product.price
product.price = data['price']
product.updated_at = datetime.utcnow()
db.session.commit()
return jsonify({
'status': 'success',
'message': 'Prix mis à jour avec succès',
'data': {
'server_id': product.id,
'barcode': product.barcode,
'old_price': float(old_price),
'new_price': float(product.price),
'updated_at': product.updated_at.isoformat() + 'Z'
}
})
// Delphi - PUT ou POST /api/products/{barcode}/price
procedure UpdatePrice(const Barcode: string; NewPrice: Currency; ServerID: Integer);
var
HttpClient: TNetHTTPClient;
Response: IHTTPResponse;
RequestBody: TJSONObject;
ResponseJSON: TJSONObject;
begin
HttpClient := TNetHTTPClient.Create(nil);
try
RequestBody := TJSONObject.Create;
try
RequestBody.AddPair('barcode', Barcode);
RequestBody.AddPair('price', TJSONNumber.Create(NewPrice));
RequestBody.AddPair('server_id', TJSONNumber.Create(ServerID));
HttpClient.ContentType := 'application/json';
HttpClient.Accept := 'application/json';
Response := HttpClient.Put(
Format('https://api.example.com/api/products/%s/price', [Barcode]),
TStringStream.Create(RequestBody.ToString, TEncoding.UTF8)
);
if Response.StatusCode = 200 then
begin
ResponseJSON := TJSONObject.ParseJSONValue(Response.ContentAsString) as TJSONObject;
try
ShowMessage('Prix mis à jour avec succès');
finally
ResponseJSON.Free;
end;
end;
finally
RequestBody.Free;
end;
finally
HttpClient.Free;
end;
end;
// WinDev - PUT ou POST /api/products/{barcode}/price
PROCEDURE MettreAJourPrix(sBarcode est une chaîne, mNouveauPrix est un monétaire, nServerID est un entier) : booléen
vRequete est un Variant
vRequete.barcode = sBarcode
vRequete.price = mNouveauPrix
vRequete.server_id = nServerID
sJSONRequete = VariantVersJSON(vRequete)
requeteHTTP est un httpRequête
requeteHTTP.URL = gsURLAPI + "/api/products/" + sBarcode + "/price"
requeteHTTP.Méthode = httpPut // ou httpPost
requeteHTTP.Contenu = sJSONRequete
tabEntetes est un tableau associatif de chaînes = ObtenirEntetes()
POUR TOUT sValeur, sCle DE tabEntetes
requeteHTTP.Entête[sCle] = sValeur
FIN
réponseHTTP est un httpRéponse = HTTPEnvoie(requeteHTTP)
SI réponseHTTP.CodeEtat = 200 ALORS
vRéponse est un Variant = JSONVersVariant(réponseHTTP.Contenu)
SI vRéponse.status = "success" ALORS
Info("Prix mis à jour", "Ancien: " + vRéponse.data.old_price, "Nouveau: " + vRéponse.data.new_price)
RENVOYER Vrai
FIN
FIN
RENVOYER Faux
FIN
Ces points de terminaison gèrent la création et la gestion des inventaires depuis IzyScan.
Objectif: Recevoir les données d'inventaire depuis IzyScan
{
"name": "Nom de l'Inventaire",
"date_created": "2024-01-15T10:30:00.000Z",
"date_started": "2024-01-15T10:35:00.000Z",
"date_finished": "2024-01-15T12:00:00.000Z",
"lines": [
{
"barcode": "1234567890123",
"quantity": 15.5,
"scanned_at": "2024-01-15T10:40:00.000Z"
}
]
}
| Champ | Type | Obligatoire | Description |
|---|---|---|---|
name |
string | Obligatoire | Nom de la session d'inventaire |
date_created |
string (ISO 8601) | Obligatoire | Date et heure de création de l'inventaire |
date_started |
string (ISO 8601) | Optionnel | Date et heure de début du scan |
date_finished |
string (ISO 8601) | Optionnel | Date et heure de fin de l'inventaire |
lines |
array | Obligatoire | Tableau des lignes d'inventaire |
| Champ | Type | Obligatoire | Description |
|---|---|---|---|
barcode |
string | Obligatoire | Code-barres du produit |
quantity |
number | Obligatoire | Quantité scannée |
scanned_at |
string (ISO 8601) | Obligatoire | Date et heure du scan de l'article |
{
"status": "success",
"message": "Inventaire créé avec succès",
"data": {
"inventory_id": 456,
"lines_processed": 25,
"total_quantity": 150.75
}
}
Statut HTTP: 201 Created
// PHP - POST /api/inventories
public function createInventory(Request $request) {
$validated = $request->validate([
'name' => 'required|string|max:100',
'date_created' => 'required|date',
'date_started' => 'nullable|date',
'date_finished' => 'nullable|date',
'lines' => 'required|array',
'lines.*.barcode' => 'required|string',
'lines.*.quantity' => 'required|numeric|min:0',
'lines.*.scanned_at' => 'required|date'
]);
DB::beginTransaction();
try {
$inventory = Inventory::create([
'name' => $validated['name'],
'date_created' => $validated['date_created'],
'date_started' => $validated['date_started'] ?? null,
'date_finished' => $validated['date_finished'] ?? null
]);
$totalQuantity = 0;
foreach ($validated['lines'] as $line) {
InventoryLine::create([
'inventory_id' => $inventory->id,
'barcode' => $line['barcode'],
'quantity' => $line['quantity'],
'scanned_at' => $line['scanned_at']
]);
$totalQuantity += $line['quantity'];
}
DB::commit();
return response()->json([
'status' => 'success',
'message' => 'Inventaire créé avec succès',
'data' => [
'inventory_id' => $inventory->id,
'lines_processed' => count($validated['lines']),
'total_quantity' => $totalQuantity
]
], 201);
} catch (\Exception $e) {
DB::rollBack();
return response()->json([
'error' => 'INTERNAL_ERROR',
'message' => 'Erreur lors de la création de l\'inventaire'
], 500);
}
}
// Node.js - POST /api/inventories
app.post('/api/inventories', async (req, res) => {
const { name, date_created, date_started, date_finished, lines } = req.body;
// Validation
if (!name || !date_created || !lines || !Array.isArray(lines)) {
return res.status(400).json({
error: 'VALIDATION_ERROR',
message: 'Données requises manquantes'
});
}
const session = await mongoose.startSession();
session.startTransaction();
try {
const inventory = new Inventory({
name,
date_created: new Date(date_created),
date_started: date_started ? new Date(date_started) : null,
date_finished: date_finished ? new Date(date_finished) : null
});
await inventory.save({ session });
let totalQuantity = 0;
const inventoryLines = [];
for (const line of lines) {
const inventoryLine = new InventoryLine({
inventory_id: inventory._id,
barcode: line.barcode,
quantity: line.quantity,
scanned_at: new Date(line.scanned_at)
});
inventoryLines.push(inventoryLine);
totalQuantity += line.quantity;
}
await InventoryLine.insertMany(inventoryLines, { session });
await session.commitTransaction();
res.status(201).json({
status: 'success',
message: 'Inventaire créé avec succès',
data: {
inventory_id: inventory._id,
lines_processed: lines.length,
total_quantity: totalQuantity
}
});
} catch (error) {
await session.abortTransaction();
res.status(500).json({
error: 'INTERNAL_ERROR',
message: 'Erreur lors de la création de l\'inventaire'
});
} finally {
session.endSession();
}
});
// C# - POST /api/inventories
[HttpPost("inventories")]
public async Task CreateInventory([FromBody] InventoryRequest request)
{
if (!ModelState.IsValid)
{
return BadRequest(new { error = "VALIDATION_ERROR", message = "Données invalides" });
}
using var transaction = await _context.Database.BeginTransactionAsync();
try
{
var inventory = new Inventory
{
Name = request.Name,
DateCreated = request.DateCreated,
DateStarted = request.DateStarted,
DateFinished = request.DateFinished
};
_context.Inventories.Add(inventory);
await _context.SaveChangesAsync();
var inventoryLines = request.Lines.Select(line => new InventoryLine
{
InventoryId = inventory.Id,
Barcode = line.Barcode,
Quantity = line.Quantity,
ScannedAt = line.ScannedAt
}).ToList();
_context.InventoryLines.AddRange(inventoryLines);
await _context.SaveChangesAsync();
await transaction.CommitAsync();
var totalQuantity = request.Lines.Sum(l => l.Quantity);
return StatusCode(201, new
{
status = "success",
message = "Inventaire créé avec succès",
data = new
{
inventory_id = inventory.Id,
lines_processed = request.Lines.Count,
total_quantity = totalQuantity
}
});
}
catch (Exception)
{
await transaction.RollbackAsync();
return StatusCode(500, new { error = "INTERNAL_ERROR", message = "Erreur lors de la création de l'inventaire" });
}
}
// Java - POST /api/inventories
@PostMapping("/api/inventories")
@Transactional
public ResponseEntity> createInventory(@RequestBody @Valid InventoryRequest request) {
try {
Inventory inventory = new Inventory();
inventory.setName(request.getName());
inventory.setDateCreated(request.getDateCreated());
inventory.setDateStarted(request.getDateStarted());
inventory.setDateFinished(request.getDateFinished());
inventory = inventoryRepository.save(inventory);
List lines = new ArrayList<>();
BigDecimal totalQuantity = BigDecimal.ZERO;
for (InventoryLineRequest lineRequest : request.getLines()) {
InventoryLine line = new InventoryLine();
line.setInventory(inventory);
line.setBarcode(lineRequest.getBarcode());
line.setQuantity(lineRequest.getQuantity());
line.setScannedAt(lineRequest.getScannedAt());
lines.add(line);
totalQuantity = totalQuantity.add(lineRequest.getQuantity());
}
inventoryLineRepository.saveAll(lines);
return ResponseEntity.status(201).body(Map.of(
"status", "success",
"message", "Inventaire créé avec succès",
"data", Map.of(
"inventory_id", inventory.getId(),
"lines_processed", lines.size(),
"total_quantity", totalQuantity
)
));
} catch (Exception e) {
return ResponseEntity.status(500).body(Map.of(
"error", "INTERNAL_ERROR",
"message", "Erreur lors de la création de l'inventaire"
));
}
}
# Python - POST /api/inventories
@app.route('/api/inventories', methods=['POST'])
def create_inventory():
data = request.get_json()
# Validation
required_fields = ['name', 'date_created', 'lines']
for field in required_fields:
if field not in data:
return jsonify({
'error': 'VALIDATION_ERROR',
'message': f'Champ requis manquant: {field}'
}), 400
try:
inventory = Inventory(
name=data['name'],
date_created=datetime.fromisoformat(data['date_created'].replace('Z', '+00:00')),
date_started=datetime.fromisoformat(data['date_started'].replace('Z', '+00:00')) if data.get('date_started') else None,
date_finished=datetime.fromisoformat(data['date_finished'].replace('Z', '+00:00')) if data.get('date_finished') else None
)
db.session.add(inventory)
db.session.flush() # Pour obtenir l'ID
total_quantity = 0
for line_data in data['lines']:
line = InventoryLine(
inventory_id=inventory.id,
barcode=line_data['barcode'],
quantity=line_data['quantity'],
scanned_at=datetime.fromisoformat(line_data['scanned_at'].replace('Z', '+00:00'))
)
db.session.add(line)
total_quantity += line_data['quantity']
db.session.commit()
return jsonify({
'status': 'success',
'message': 'Inventaire créé avec succès',
'data': {
'inventory_id': inventory.id,
'lines_processed': len(data['lines']),
'total_quantity': total_quantity
}
}), 201
except Exception as e:
db.session.rollback()
return jsonify({
'error': 'INTERNAL_ERROR',
'message': 'Erreur lors de la création de l\'inventaire'
}), 500
// Delphi - POST /api/inventories
procedure CreateInventory(const InventoryData: TInventoryRequest);
var
HttpClient: TNetHTTPClient;
Response: IHTTPResponse;
RequestJSON, LineJSON: TJSONObject;
LinesArray: TJSONArray;
Line: TInventoryLine;
ResponseJSON: TJSONObject;
begin
HttpClient := TNetHTTPClient.Create(nil);
try
RequestJSON := TJSONObject.Create;
try
RequestJSON.AddPair('name', InventoryData.Name);
RequestJSON.AddPair('date_created', DateTimeToISO8601(InventoryData.DateCreated));
if InventoryData.DateStarted > 0 then
RequestJSON.AddPair('date_started', DateTimeToISO8601(InventoryData.DateStarted));
if InventoryData.DateFinished > 0 then
RequestJSON.AddPair('date_finished', DateTimeToISO8601(InventoryData.DateFinished));
LinesArray := TJSONArray.Create;
for Line in InventoryData.Lines do
begin
LineJSON := TJSONObject.Create;
LineJSON.AddPair('barcode', Line.Barcode);
LineJSON.AddPair('quantity', TJSONNumber.Create(Line.Quantity));
LineJSON.AddPair('scanned_at', DateTimeToISO8601(Line.ScannedAt));
LinesArray.AddElement(LineJSON);
end;
RequestJSON.AddPair('lines', LinesArray);
HttpClient.ContentType := 'application/json';
HttpClient.Accept := 'application/json';
Response := HttpClient.Post(
'https://api.example.com/api/inventories',
TStringStream.Create(RequestJSON.ToString, TEncoding.UTF8)
);
if Response.StatusCode = 201 then
begin
ResponseJSON := TJSONObject.ParseJSONValue(Response.ContentAsString) as TJSONObject;
try
if ResponseJSON.GetValue('status').Value = 'success' then
ShowMessage('Inventaire créé avec succès');
finally
ResponseJSON.Free;
end;
end;
finally
RequestJSON.Free;
end;
finally
HttpClient.Free;
end;
end;
// WinDev - POST /api/inventories
PROCEDURE CréerInventaire(sNom est une chaîne, dtDateCreated est un DateHeure, dtDateStarted est un DateHeure = "", dtDateFinished est un DateHeure = "", tabLignes est un tableau de STInventoryLine) : entier
vInventaire est un Variant
vInventaire.name = sNom
vInventaire.date_created = DateHeureVersISO8601(dtDateCreated)
SI dtDateStarted <> "" ALORS
vInventaire.date_started = DateHeureVersISO8601(dtDateStarted)
FIN
SI dtDateFinished <> "" ALORS
vInventaire.date_finished = DateHeureVersISO8601(dtDateFinished)
FIN
Dimension(vInventaire.lines, 0)
POUR TOUT stLigne DE tabLignes
vLigne est un Variant
vLigne.barcode = stLigne.barcode
vLigne.quantity = stLigne.quantity
vLigne.scanned_at = stLigne.scanned_at
Ajoute(vInventaire.lines, vLigne)
FIN
sJSONRequete = VariantVersJSON(vInventaire)
requeteHTTP est un httpRequête
requeteHTTP.URL = gsURLAPI + "/api/inventories"
requeteHTTP.Méthode = httpPost
requeteHTTP.Contenu = sJSONRequete
tabEntetes est un tableau associatif de chaînes = ObtenirEntetes()
POUR TOUT sValeur, sCle DE tabEntetes
requeteHTTP.Entête[sCle] = sValeur
FIN
réponseHTTP est un httpRéponse = HTTPEnvoie(requeteHTTP)
SI réponseHTTP.CodeEtat = 201 ALORS
vRéponse est un Variant = JSONVersVariant(réponseHTTP.Contenu)
SI vRéponse.status = "success" ALORS
Info("Inventaire créé", "ID: " + vRéponse.data.inventory_id)
RENVOYER vRéponse.data.inventory_id
FIN
FIN
RENVOYER 0
FIN
| Champ | Type | Obligatoire | Description |
|---|---|---|---|
id |
string/number | Obligatoire | Identifiant unique du produit (peut être identique au barcode) |
name |
string | Obligatoire | Nom d'affichage du produit |
barcode |
string | Obligatoire | Code-barres du produit (EAN-13, UPC, etc.). Les produits sans code-barres sont ignorés. |
price |
number | Obligatoire | Prix de vente actuel |
promo_price |
number | Optionnel | Prix promotionnel (si applicable) |
server_id |
string/number | Optionnel | ID du produit côté serveur (utilisé pour les mises à jour de prix) |
| Champ | Type | Obligatoire | Description |
|---|---|---|---|
name |
string | Obligatoire | Nom de la session d'inventaire |
date_created |
string (ISO 8601) | Obligatoire | Date et heure de création de l'inventaire |
date_started |
string (ISO 8601) | Optionnel | Date et heure de début du scan |
date_finished |
string (ISO 8601) | Optionnel | Date et heure de fin de l'inventaire |
lines |
array | Obligatoire | Tableau des lignes d'inventaire |
| Champ | Type | Obligatoire | Description |
|---|---|---|---|
barcode |
string | Obligatoire | Code-barres du produit |
quantity |
number | Obligatoire | Quantité scannée |
scanned_at |
string (ISO 8601) | Obligatoire | Date et heure du scan de l'article |
{
"error": "CODE_ERREUR",
"message": "Message d'erreur lisible"
}
| Code | Description |
|---|---|
| 200 OK | Requêtes GET/PUT réussies |
| 201 Created | Requêtes POST réussies |
| 400 Bad Request | Données de requête invalides |
| 401 Unauthorized | Authentification requise ou échouée |
| 404 Not Found | Ressource non trouvée |
| 422 Unprocessable Entity | Erreurs de validation |
| 500 Internal Server Error | Erreur serveur |
INVALID_CREDENTIALS - Authentification échouéePRODUCT_NOT_FOUND - Produit avec le code-barres donné non trouvéINVALID_BARCODE - Format du code-barres invalideINVALID_PRICE - Valeur du prix invalideDUPLICATE_BARCODE - Le code-barres existe déjàVALIDATION_ERROR - Validation des données échouéeINTERNAL_ERROR - Erreur interne du serveurCette section décrit les flux typiques d'utilisation de l'API par le client IzyScan.
GET /api/ping pour vérifier la connectivitéGET /api/products?limit=2000&offset=0has_more soit falseGET /api/products?updated_since=2024-01-15 10:30:00&limit=2000&offset=0POST /api/inventories avec les données complètes de l'inventairePUT /api/products/{barcode}/priceupdated_since améliore significativement les performances de synchronisation pour les grands cataloguesbarcode, updated_at{
"test_product": {
"id": 999,
"name": "Produit Test",
"barcode": "9999999999999",
"price": 10.99,
"promo_price": 8.99,
"server_id": 999
},
"test_inventory": {
"name": "Inventaire Test",
"date_created": "2024-01-15T10:30:00.000Z",
"date_started": "2024-01-15T10:35:00.000Z",
"date_finished": "2024-01-15T12:00:00.000Z",
"lines": [
{
"barcode": "9999999999999",
"quantity": 5.0,
"scanned_at": "2024-01-15T10:40:00.000Z"
}
]
}
}
Pour le support technique et les questions concernant cette intégration: