Intégration de WeChat Pay en .NET Core

Présentation de l'intégration WeChat Pay en .NET Core

Cet article explore l'intégration de la plateforme de paiement WeChat Pay au sein d'une application .NET Core. La mise en œuvre de WeChat Pay diffère d'autres systèmes en ce qu'elle nécessite la création préalable d'un ordre de pré-paiement, suivie par l'invocation d'API spécifiques pour finaliser la transaction. Nous allons détailler les copmosants clés pour gérer les interactions avec les services WeChat Pay.

1. Structure de Données pour les Échanges API WeChat Pay (PayloadPaiementWeChat)

La classe PayloadPaiementWeChat est un élément central pour encapsuler les paramètres des requêtes et réponses API. Elle utilise un SortedDictionary<string, object> pour stocker les paires clé-valeur, ce qui est crucial pour garantir l'ordre alphabétique requis lors de la génération des signatures.

using System.Collections.Generic;
using System.Security.Cryptography;
using System.Text;
using System.Xml;
using Newtonsoft.Json; // Assurez-vous d'avoir le package NuGet Newtonsoft.Json

namespace Qwerty.Payment.WeChatPay
{
    internal class PayloadPaiementWeChat
    {
        private readonly SortedDictionary<string, object> _parametres = new SortedDictionary<string, object>();

        /// <summary>
        /// Définit la valeur d'un paramètre.
        /// </summary>
        /// <param name="cle">Nom du paramètre.</param>
        /// <param name="valeur">Valeur du paramètre.</param>
        public void DefinirParametre(string cle, object valeur)
        {
            _parametres[cle] = valeur;
        }

        /// <summary>
        /// Récupère la valeur d'un paramètre par sa clé.
        /// </summary>
        /// <param name="cle">Nom du paramètre.</param>
        /// <returns>La valeur du paramètre, ou null si non trouvé.</returns>
        public object ObtenirParametre(string cle)
        {
            _parametres.TryGetValue(cle, out object valeur);
            return valeur;
        }

        /// <summary>
        /// Vérifie si un paramètre est défini.
        /// </summary>
        /// <param name="cle">Nom du paramètre.</param>
        /// <returns>True si le paramètre est défini, sinon false.</returns>
        public bool ContientParametre(string cle)
        {
            return ObtenirParametre(cle) != null;
        }

        /// <summary>
        /// Convertit les paramètres en une chaîne XML.
        /// </summary>
        /// <returns>La chaîne XML résultante.</returns>
        /// <exception cref="System.Exception">Lancée si les données sont vides ou contiennent des valeurs nulles.</exception>
        public string VersXml()
        {
            if (_parametres.Count == 0)
            {
                throw new System.Exception("Les données de paiement WeChat sont vides pour la conversion XML !");
            }

            var xmlBuilder = new StringBuilder("<xml>");
            foreach (var paire in _parametres)
            {
                if (paire.Value == null)
                {
                    throw new System.Exception($"Le paramètre '{paire.Key}' ne peut pas être null pour la conversion XML !");
                }

                if (paire.Value is int)
                {
                    xmlBuilder.Append($"<{paire.Key}>{paire.Value}</{paire.Key}>");
                }
                else if (paire.Value is string)
                {
                    xmlBuilder.Append($"<{paire.Key}><![CDATA[{paire.Value}]]></{paire.Key}>");
                }
                else
                {
                    throw new System.Exception($"Type de données invalide pour le paramètre '{paire.Key}'. Seuls les entiers et les chaînes sont supportés.");
                }
            }
            xmlBuilder.Append("</xml>");
            return xmlBuilder.ToString();
        }

        /// <summary>
        /// Parse une chaîne XML et peuple les paramètres internes.
        /// </summary>
        /// <param name="xml">La chaîne XML à parser.</param>
        /// <returns>Le dictionnaire des paramètres parsés.</returns>
        /// <exception cref="System.Exception">Lancée si l'XML est invalide ou la signature incorrecte.</exception>
        public SortedDictionary<string, object> DepuisXml(string xml)
        {
            if (string.IsNullOrEmpty(xml))
            {
                throw new System.Exception("Impossible de convertir une chaîne XML vide en données de paiement WeChat.");
            }

            var docXml = new XmlDocument();
            docXml.LoadXml(xml);
            var noeudRacine = docXml.FirstChild; // Nœud <xml>
            var noeudsEnfants = noeudRacine.ChildNodes;

            _parametres.Clear(); // Nettoyage avant de remplir
            foreach (XmlNode noeud in noeudsEnfants)
            {
                if (noeud is XmlElement element)
                {
                    _parametres[element.Name] = element.InnerText;
                }
            }

            try
            {
                if (ContientParametre("return_code") && ObtenirParametre("return_code").ToString() != "SUCCESS")
                {
                    // Ne pas vérifier la signature si la communication initiale a échoué.
                    return _parametres;
                }
                VerifierSignature(); // Vérifie la signature du XML reçu
            }
            catch (System.Exception ex)
            {
                throw new System.Exception("Erreur lors de la vérification de la signature après la conversion XML.", ex);
            }

            return _parametres;
        }

        /// <summary>
        /// Convertit les paramètres en format de chaîne d'URL (hors signature).
        /// </summary>
        /// <returns>La chaîne d'URL.</returns>
        public string VersChaineUrl()
        {
            var urlBuilder = new StringBuilder();
            foreach (var paire in _parametres)
            {
                if (paire.Value == null)
                {
                    throw new System.Exception($"Le paramètre '{paire.Key}' ne peut pas être null lors de la conversion en URL !");
                }
                if (paire.Key != "sign" && !string.IsNullOrEmpty(paire.Value.ToString()))
                {
                    urlBuilder.Append($"{paire.Key}={paire.Value}&");
                }
            }
            return urlBuilder.ToString().TrimEnd('&');
        }

        /// <summary>
        /// Convertit les paramètres en format JSON.
        /// </summary>
        /// <returns>La chaîne JSON.</returns>
        public string VersJson()
        {
            return JsonConvert.SerializeObject(_parametres);
        }

        /// <summary>
        /// Génère la signature SHA256 (ou MD5 selon la configuration) des paramètres.
        /// </summary>
        /// <returns>La signature en majuscules.</returns>
        public string GenererSignature()
        {
            string parametresPourSignature = VersChaineUrl();
            parametresPourSignature += "&key=" + ConfigurationPaiementWeChat.CleAPI;

            using (var md5 = MD5.Create())
            {
                byte[] hashBytes = md5.ComputeHash(Encoding.UTF8.GetBytes(parametresPourSignature));
                var signatureBuilder = new StringBuilder();
                foreach (byte b in hashBytes)
                {
                    signatureBuilder.Append(b.ToString("x2"));
                }
                return signatureBuilder.ToString().ToUpper();
            }
        }

        /// <summary>
        /// Vérifie la validité de la signature reçue.
        /// </summary>
        /// <returns>True si la signature est correcte.</returns>
        /// <exception cref="System.Exception">Lancée si la signature est manquante ou invalide.</exception>
        public bool VerifierSignature()
        {
            if (!ContientParametre("sign"))
            {
                throw new System.Exception("La signature 'sign' est manquante dans les données WeChat Pay !");
            }
            if (string.IsNullOrEmpty(ObtenirParametre("sign").ToString()))
            {
                throw new System.Exception("La signature 'sign' est présente mais vide dans les données WeChat Pay !");
            }

            string signatureRecue = ObtenirParametre("sign").ToString();
            string signatureCalculee = GenererSignature();

            if (signatureCalculee == signatureRecue)
            {
                return true;
            }

            throw new System.Exception("La vérification de la signature des données WeChat Pay a échoué !");
        }

        /// <summary>
        /// Retourne le dictionnaire interne des paramètres.
        /// </summary>
        /// <returns>Le dictionnaire de paramètres.</returns>
        public SortedDictionary<string, object> ObtenirTousLesParametres()
        {
            return _parametres;
        }
    }
}

2. Client API WeChat Pay (ClientApiWeChatPay)

Cette classe fournit une interface programmatique pour interagir avec les diverses API de paiement WeChat. Elle automatise l'ajout des informations de configuration communes (App ID, MCH ID, chaîne aléatoire, signature) et gère les communications HTTP.

using System;
using System.Text;
using System.Threading.Tasks;

namespace Qwerty.Payment.WeChatPay
{
    internal static class ClientApiWeChatPay
    {
        /// <summary>
        /// Appel de l'API de Micro-paiement (paiement par code scanné).
        /// </summary>
        /// <param name="input">Données de la requête.</param>
        /// <param name="delaiExpiration">Délai d'expiration en secondes.</param>
        /// <returns>Les données de la réponse.</returns>
        /// <exception cref="System.Exception">Lancée si des paramètres obligatoires sont manquants.</exception>
        public static async Task<PayloadPaiementWeChat> ExecuterMicroPaiement(PayloadPaiementWeChat input, int delaiExpiration = 10)
        {
            const string urlApi = "https://api.mch.weixin.qq.com/pay/micropay";

            // Vérification des paramètres obligatoires
            if (!input.ContientParametre("body") || !input.ContientParametre("out_trade_no") ||
                !input.ContientParametre("total_fee") || !input.ContientParametre("auth_code"))
            {
                throw new System.Exception("Paramètres 'body', 'out_trade_no', 'total_fee' ou 'auth_code' manquants pour le micro-paiement !");
            }

            input.DefinirParametre("spbill_create_ip", ConfigurationPaiementWeChat.AdresseIP);
            input.DefinirParametre("appid", ConfigurationPaiementWeChat.AppId);
            input.DefinirParametre("mch_id", ConfigurationPaiementWeChat.MarchandId);
            input.DefinirParametre("nonce_str", GenererChaineAleatoire());
            input.DefinirParametre("sign", input.GenererSignature());

            string xmlRequete = input.VersXml();
            DateTime debutRequete = DateTime.Now;

            string reponseXml = await ClientHttpWeChat.EnvoyerPost(xmlRequete, urlApi, false, delaiExpiration);

            DateTime finRequete = DateTime.Now;
            int coutTempsMs = (int)(finRequete - debutRequete).TotalMilliseconds;

            var resultat = new PayloadPaiementWeChat();
            resultat.DepuisXml(reponseXml);

            RapporterTempsExecution(urlApi, coutTempsMs, resultat);

            return resultat;
        }

        /// <summary>
        /// Requête de commande.
        /// </summary>
        /// <param name="input">Données de la requête.</param>
        /// <param name="delaiExpiration">Délai d'expiration en secondes.</param>
        /// <returns>Les données de la réponse.</returns>
        /// <exception cref="System.Exception">Lancée si 'out_trade_no' ou 'transaction_id' sont manquants.</exception>
        public static async Task<PayloadPaiementWeChat> RequerirCommande(PayloadPaiementWeChat input, int delaiExpiration = 6)
        {
            const string urlApi = "https://api.mch.weixin.qq.com/pay/orderquery";

            if (!input.ContientParametre("out_trade_no") && !input.ContientParametre("transaction_id"))
            {
                throw new System.Exception("L'un des paramètres 'out_trade_no' ou 'transaction_id' est requis pour la requête de commande !");
            }

            input.DefinirParametre("appid", ConfigurationPaiementWeChat.AppId);
            input.DefinirParametre("mch_id", ConfigurationPaiementWeChat.MarchandId);
            input.DefinirParametre("nonce_str", GenererChaineAleatoire());
            input.DefinirParametre("sign", input.GenererSignature());

            string xmlRequete = input.VersXml();
            DateTime debutRequete = DateTime.Now;

            string reponseXml = await ClientHttpWeChat.EnvoyerPost(xmlRequete, urlApi, false, delaiExpiration);

            DateTime finRequete = DateTime.Now;
            int coutTempsMs = (int)(finRequete - debutRequete).TotalMilliseconds;

            var resultat = new PayloadPaiementWeChat();
            resultat.DepuisXml(reponseXml);

            RapporterTempsExecution(urlApi, coutTempsMs, resultat);

            return resultat;
        }

        /// <summary>
        /// Annulation de commande (nécessite un certificat).
        /// </summary>
        /// <param name="input">Données de la requête.</param>
        /// <param name="delaiExpiration">Délai d'expiration en secondes.</param>
        /// <returns>Les données de la réponse.</returns>
        /// <exception cref="System.Exception">Lancée si 'out_trade_no' ou 'transaction_id' sont manquants.</exception>
        public static async Task<PayloadPaiementWeChat> AnnulerCommande(PayloadPaiementWeChat input, int delaiExpiration = 6)
        {
            const string urlApi = "https://api.mch.weixin.qq.com/secapi/pay/reverse";

            if (!input.ContientParametre("out_trade_no") && !input.ContientParametre("transaction_id"))
            {
                throw new System.Exception("L'un des paramètres 'out_trade_no' ou 'transaction_id' est requis pour annuler la commande !");
            }

            input.DefinirParametre("appid", ConfigurationPaiementWeChat.AppId);
            input.DefinirParametre("mch_id", ConfigurationPaiementWeChat.MarchandId);
            input.DefinirParametre("nonce_str", GenererChaineAleatoire());
            input.DefinirParametre("sign", input.GenererSignature());
            string xmlRequete = input.VersXml();

            DateTime debutRequete = DateTime.Now;
            string reponseXml = await ClientHttpWeChat.EnvoyerPost(xmlRequete, urlApi, true, delaiExpiration); // Nécessite un certificat
            DateTime finRequete = DateTime.Now;
            int coutTempsMs = (int)(finRequete - debutRequete).TotalMilliseconds;

            var resultat = new PayloadPaiementWeChat();
            resultat.DepuisXml(reponseXml);

            RapporterTempsExecution(urlApi, coutTempsMs, resultat);

            return resultat;
        }

        /// <summary>
        /// Demande de remboursement (nécessite un certificat).
        /// </summary>
        /// <param name="input">Données de la requête.</param>
        /// <param name="delaiExpiration">Délai d'expiration en secondes.</param>
        /// <returns>Les données de la réponse.</returns>
        /// <exception cref="System.Exception">Lancée si des paramètres obligatoires sont manquants.</exception>
        public static async Task<PayloadPaiementWeChat> DemanderRemboursement(PayloadPaiementWeChat input, int delaiExpiration = 6)
        {
            const string urlApi = "https://api.mch.weixin.qq.com/secapi/pay/refund";

            if (!input.ContientParametre("out_trade_no") && !input.ContientParametre("transaction_id"))
            {
                throw new System.Exception("L'un des paramètres 'out_trade_no' ou 'transaction_id' est requis pour le remboursement !");
            }
            else if (!input.ContientParametre("out_refund_no") || !input.ContientParametre("total_fee") ||
                     !input.ContientParametre("refund_fee") || !input.ContientParametre("op_user_id"))
            {
                throw new System.Exception("Paramètres 'out_refund_no', 'total_fee', 'refund_fee' ou 'op_user_id' manquants pour le remboursement !");
            }

            input.DefinirParametre("appid", ConfigurationPaiementWeChat.AppId);
            input.DefinirParametre("mch_id", ConfigurationPaiementWeChat.MarchandId);
            input.DefinirParametre("nonce_str", GenererChaineAleatoire());
            input.DefinirParametre("sign", input.GenererSignature());

            string xmlRequete = input.VersXml();
            DateTime debutRequete = DateTime.Now;

            string reponseXml = await ClientHttpWeChat.EnvoyerPost(xmlRequete, urlApi, true, delaiExpiration); // Nécessite un certificat

            DateTime finRequete = DateTime.Now;
            int coutTempsMs = (int)(finRequete - debutRequete).TotalMilliseconds;

            var resultat = new PayloadPaiementWeChat();
            resultat.DepuisXml(reponseXml);

            RapporterTempsExecution(urlApi, coutTempsMs, resultat);

            return resultat;
        }

        /// <summary>
        /// Requête de statut de remboursement.
        /// </summary>
        /// <param name="input">Données de la requête.</param>
        /// <param name="delaiExpiration">Délai d'expiration en secondes.</param>
        /// <returns>Les données de la réponse.</returns>
        /// <exception cref="System.Exception">Lancée si aucun des paramètres d'identification n'est présent.</exception>
        public static async Task<PayloadPaiementWeChat> RequerirRemboursement(PayloadPaiementWeChat input, int delaiExpiration = 6)
        {
            const string urlApi = "https://api.mch.weixin.qq.com/pay/refundquery";

            if (!input.ContientParametre("out_refund_no") && !input.ContientParametre("out_trade_no") &&
                !input.ContientParametre("transaction_id") && !input.ContientParametre("refund_id"))
            {
                throw new System.Exception("L'un des paramètres 'out_refund_no', 'out_trade_no', 'transaction_id' ou 'refund_id' est requis pour la requête de remboursement !");
            }

            input.DefinirParametre("appid", ConfigurationPaiementWeChat.AppId);
            input.DefinirParametre("mch_id", ConfigurationPaiementWeChat.MarchandId);
            input.DefinirParametre("nonce_str", GenererChaineAleatoire());
            input.DefinirParametre("sign", input.GenererSignature());

            string xmlRequete = input.VersXml();
            DateTime debutRequete = DateTime.Now;

            string reponseXml = await ClientHttpWeChat.EnvoyerPost(xmlRequete, urlApi, false, delaiExpiration);

            DateTime finRequete = DateTime.Now;
            int coutTempsMs = (int)(finRequete - debutRequete).TotalMilliseconds;

            var resultat = new PayloadPaiementWeChat();
            resultat.DepuisXml(reponseXml);

            RapporterTempsExecution(urlApi, coutTempsMs, resultat);

            return resultat;
        }

        /// <summary>
        /// Téléchargement du relevé de compte.
        /// </summary>
        /// <param name="input">Données de la requête.</param>
        /// <param name="delaiExpiration">Délai d'expiration en secondes.</param>
        /// <returns>Les données de la réponse.</returns>
        /// <exception cref="System.Exception">Lancée si 'bill_date' est manquant.</exception>
        public static async Task<PayloadPaiementWeChat> TelechargerReleve(PayloadPaiementWeChat input, int delaiExpiration = 6)
        {
            const string urlApi = "https://api.mch.weixin.qq.com/pay/downloadbill";

            if (!input.ContientParametre("bill_date"))
            {
                throw new System.Exception("Le paramètre 'bill_date' est requis pour le téléchargement du relevé de compte !");
            }

            input.DefinirParametre("appid", ConfigurationPaiementWeChat.AppId);
            input.DefinirParametre("mch_id", ConfigurationPaiementWeChat.MarchandId);
            input.DefinirParametre("nonce_str", GenererChaineAleatoire());
            input.DefinirParametre("sign", input.GenererSignature());

            string xmlRequete = input.VersXml();
            string reponse = await ClientHttpWeChat.EnvoyerPost(xmlRequete, urlApi, false, delaiExpiration);

            var resultat = new PayloadPaiementWeChat();
            // Si la requête échoue, la réponse est au format XML
            if (reponse.StartsWith("<xml>"))
            {
                resultat.DepuisXml(reponse);
            }
            else // Si elle réussit, la réponse est le contenu du relevé
            {
                resultat.DefinirParametre("result", reponse);
            }

            return resultat;
        }

        /// <summary>
        /// Conversion d'URL longue en URL courte.
        /// </summary<
        /// <param name="input">Données de la requête.</param>
        /// <param name="delaiExpiration">Délai d'expiration en secondes.</param>
        /// <returns>Les données de la réponse.</returns>
        /// <exception cref="System.Exception">Lancée si 'long_url' est manquant.</exception>
        public static async Task<PayloadPaiementWeChat> ConvertirUrlCourte(PayloadPaiementWeChat input, int delaiExpiration = 6)
        {
            const string urlApi = "https://api.mch.weixin.qq.com/tools/shorturl";

            if (!input.ContientParametre("long_url"))
            {
                throw new System.Exception("Le paramètre 'long_url' est requis pour la conversion en URL courte !");
            }

            input.DefinirParametre("appid", ConfigurationPaiementWeChat.AppId);
            input.DefinirParametre("mch_id", ConfigurationPaiementWeChat.MarchandId);
            input.DefinirParametre("nonce_str", GenererChaineAleatoire());
            input.DefinirParametre("sign", input.GenererSignature());
            string xmlRequete = input.VersXml();

            DateTime debutRequete = DateTime.Now;
            string reponseXml = await ClientHttpWeChat.EnvoyerPost(xmlRequete, urlApi, false, delaiExpiration);
            DateTime finRequete = DateTime.Now;
            int coutTempsMs = (int)(finRequete - debutRequete).TotalMilliseconds;

            var resultat = new PayloadPaiementWeChat();
            resultat.DepuisXml(reponseXml);
            RapporterTempsExecution(urlApi, coutTempsMs, resultat);

            return resultat;
        }

        /// <summary>
        /// Commande unifiée (API principale pour la plupart des paiements).
        /// </summary>
        /// <param name="input">Données de la requête.</param>
        /// <param name="delaiExpiration">Délai d'expiration en secondes.</param>
        /// <returns>Les données de la réponse.</returns>
        /// <exception cref="System.Exception">Lancée si des paramètres obligatoires sont manquants.</exception>
        public static async Task<PayloadPaiementWeChat> ExecuterCommandeUnifiee(PayloadPaiementWeChat input, int delaiExpiration = 6)
        {
            const string urlApi = "https://api.mch.weixin.qq.com/pay/unifiedorder";

            if (!input.ContientParametre("out_trade_no") || !input.ContientParametre("body") ||
                !input.ContientParametre("total_fee") || !input.ContientParametre("trade_type"))
            {
                throw new System.Exception("Paramètres 'out_trade_no', 'body', 'total_fee' ou 'trade_type' manquants pour la commande unifiée !");
            }

            // Paramètres spécifiques au type de transaction
            if (input.ObtenirParametre("trade_type").ToString() == "JSAPI" && !input.ContientParametre("openid"))
            {
                throw new System.Exception("Le paramètre 'openid' est obligatoire pour le type de transaction JSAPI !");
            }
            if (input.ObtenirParametre("trade_type").ToString() == "NATIVE" && !input.ContientParametre("product_id"))
            {
                throw new System.Exception("Le paramètre 'product_id' est obligatoire pour le type de transaction NATIVE !");
            }

            // Utilise l'URL de notification par défaut si non spécifiée
            if (!input.ContientParametre("notify_url"))
            {
                input.DefinirParametre("notify_url", ConfigurationPaiementWeChat.UrlNotification);
            }

            input.DefinirParametre("appid", ConfigurationPaiementWeChat.AppId);
            input.DefinirParametre("mch_id", ConfigurationPaiementWeChat.MarchandId);
            input.DefinirParametre("nonce_str", GenererChaineAleatoire());
            input.DefinirParametre("sign", input.GenererSignature());
            string xmlRequete = input.VersXml();

            DateTime debutRequete = DateTime.Now;
            string reponseXml = await ClientHttpWeChat.EnvoyerPost(xmlRequete, urlApi, false, delaiExpiration);
            DateTime finRequete = DateTime.Now;
            int coutTempsMs = (int)(finRequete - debutRequete).TotalMilliseconds;

            var resultat = new PayloadPaiementWeChat();
            resultat.DepuisXml(reponseXml);
            RapporterTempsExecution(urlApi, coutTempsMs, resultat);

            return resultat;
        }

        /// <summary>
        /// Fermeture de commande.
        /// </summary>
        /// <param name="input">Données de la requête.</param>
        /// <param name="delaiExpiration">Délai d'expiration en secondes.</param>
        /// <returns>Les données de la réponse.</returns>
        /// <exception cref="System.Exception">Lancée si 'out_trade_no' est manquant.</exception>
        public static async Task<PayloadPaiementWeChat> FermerCommande(PayloadPaiementWeChat input, int delaiExpiration = 6)
        {
            const string urlApi = "https://api.mch.weixin.qq.com/pay/closeorder";

            if (!input.ContientParametre("out_trade_no"))
            {
                throw new System.Exception("Le paramètre 'out_trade_no' est requis pour la fermeture de commande !");
            }

            input.DefinirParametre("appid", ConfigurationPaiementWeChat.AppId);
            input.DefinirParametre("mch_id", ConfigurationPaiementWeChat.MarchandId);
            input.DefinirParametre("nonce_str", GenererChaineAleatoire());
            input.DefinirParametre("sign", input.GenererSignature());
            string xmlRequete = input.VersXml();

            DateTime debutRequete = DateTime.Now;
            string reponseXml = await ClientHttpWeChat.EnvoyerPost(xmlRequete, urlApi, false, delaiExpiration);
            DateTime finRequete = DateTime.Now;
            int coutTempsMs = (int)(finRequete - debutRequete).TotalMilliseconds;

            var resultat = new PayloadPaiementWeChat();
            resultat.DepuisXml(reponseXml);
            RapporterTempsExecution(urlApi, coutTempsMs, resultat);

            return resultat;
        }

        /// <summary>
        /// Rapporte les temps d'exécution des API.
        /// </summary>
        /// <param name="urlInterface">URL de l'API.</param>
        /// <param name="coutTemps">Coût en temps (ms).</param>
        /// <param name="donneesResultat">Données de résultat de l'API.</param>
        private static async void RapporterTempsExecution(string urlInterface, int coutTemps, PayloadPaiementWeChat donneesResultat)
        {
            // La logique de rapport peut être configurée (par ex. rapporter seulement les échecs)
            if (ConfigurationPaiementWeChat.NiveauRapport == 0) return; // Rapport désactivé

            if (ConfigurationPaiementWeChat.NiveauRapport == 1 &&
                donneesResultat.ContientParametre("return_code") && donneesResultat.ObtenirParametre("return_code").ToString() == "SUCCESS" &&
                donneesResultat.ContientParametre("result_code") && donneesResultat.ObtenirParametre("result_code").ToString() == "SUCCESS")
            {
                return; // Rapporte uniquement les erreurs si le niveau est 1
            }

            var donneesRapport = new PayloadPaiementWeChat();
            donneesRapport.DefinirParametre("interface_url", urlInterface);
            donneesRapport.DefinirParametre("execute_time_", coutTemps);

            if (donneesResultat.ContientParametre("return_code")) donneesRapport.DefinirParametre("return_code", donneesResultat.ObtenirParametre("return_code"));
            if (donneesResultat.ContientParametre("return_msg")) donneesRapport.DefinirParametre("return_msg", donneesResultat.ObtenirParametre("return_msg"));
            if (donneesResultat.ContientParametre("result_code")) donneesRapport.DefinirParametre("result_code", donneesResultat.ObtenirParametre("result_code"));
            if (donneesResultat.ContientParametre("err_code")) donneesRapport.DefinirParametre("err_code", donneesResultat.ObtenirParametre("err_code"));
            if (donneesResultat.ContientParametre("err_code_des")) donneesRapport.DefinirParametre("err_code_des", donneesResultat.ObtenirParametre("err_code_des"));
            if (donneesResultat.ContientParametre("out_trade_no")) donneesRapport.DefinirParametre("out_trade_no", donneesResultat.ObtenirParametre("out_trade_no"));
            if (donneesResultat.ContientParametre("device_info")) donneesRapport.DefinirParametre("device_info", donneesResultat.ObtenirParametre("device_info"));

            try
            {
                await EnvoyerRapport(donneesRapport);
            }
            catch (System.Exception)
            {
                // Ignorer les erreurs de rapport pour ne pas affecter le flux principal
            }
        }

        /// <summary>
        /// Envoi d'un rapport de performance à WeChat Pay.
        /// </summary>
        /// <param name="input">Données du rapport.</param>
        /// <param name="delaiExpiration">Délai d'expiration en secondes.</param>
        /// <returns>Les données de la réponse.</returns>
        /// <exception cref="System.Exception">Lancée si des paramètres obligatoires sont manquants.</exception>
        public static async Task<PayloadPaiementWeChat> EnvoyerRapport(PayloadPaiementWeChat input, int delaiExpiration = 1)
        {
            const string urlApi = "https://api.mch.weixin.qq.com/payitil/report";

            if (!input.ContientParametre("interface_url") || !input.ContientParametre("return_code") ||
                !input.ContientParametre("result_code") || !input.ContientParametre("user_ip") ||
                !input.ContientParametre("execute_time_"))
            {
                throw new System.Exception("Paramètres requis pour le rapport manquants !");
            }

            input.DefinirParametre("appid", ConfigurationPaiementWeChat.AppId);
            input.DefinirParametre("mch_id", ConfigurationPaiementWeChat.MarchandId);
            input.DefinirParametre("user_ip", ConfigurationPaiementWeChat.AdresseIP);
            input.DefinirParametre("time", DateTime.Now.ToString("yyyyMMddHHmmss"));
            input.DefinirParametre("nonce_str", GenererChaineAleatoire());
            input.DefinirParametre("sign", input.GenererSignature());
            string xmlRequete = input.VersXml();

            string reponseXml = await ClientHttpWeChat.EnvoyerPost(xmlRequete, urlApi, false, delaiExpiration);

            var resultat = new PayloadPaiementWeChat();
            resultat.DepuisXml(reponseXml);
            return resultat;
        }

        /// <summary>
        /// Génère un numéro de commande externe unique.
        /// </summary>
        /// <returns>Numéro de commande.</returns>
        public static string GenererNumeroCommandeExterne()
        {
            var rnd = new Random();
            return $"{ConfigurationPaiementWeChat.MarchandId}{DateTime.Now:yyyyMMddHHmmss}{rnd.Next(99999).ToString().PadLeft(5, '0')}";
        }

        /// <summary>
        /// Génère un horodatage Unix (secondes depuis 1970).
        /// </summary>
        /// <returns>L'horodatage.</returns>
        public static string GenererHorodatage()
        {
            TimeSpan ts = DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc);
            return Convert.ToInt64(ts.TotalSeconds).ToString();
        }

        /// <summary>
        /// Génère une chaîne aléatoire (nonce string).
        /// </summary>
        /// <returns>La chaîne aléatoire.</returns>
        public static string GenererChaineAleatoire()
        {
            return Guid.NewGuid().ToString("N"); // 'N' for No hyphens
        }
    }
}

3. Classes de Traitemant des Paiements Spécifiques

Chaque scénario de paiement WeChat a ses propres exigences, gérées par des classes dédiées.

3.1. Paiement via Application Mobile (GestionnairePaiementApp)

Pour le paiement en application, après la commande unifiée, une seconde signature est nécessaire pour les paramètres passés au SDK WeChat côté client mobile.

using System;
using System.Threading.Tasks;
using System.Collections.Generic;
using Newtonsoft.Json;

namespace Qwerty.Payment.WeChatPay
{
    internal class GestionnairePaiementApp
    {
        /// <summary>
        /// Exécute le flux de commande unifiée pour un paiement via application mobile.
        /// </summary>
        /// <param name="description">Description du produit.</param>
        /// <param name="refCommandeMarchand">Numéro de commande du marchand.</param>
        /// <param name="montantTotal">Montant total de la commande en cents.</param>
        /// <returns>Les données de la réponse de la commande unifiée.</returns>
        public async Task<PayloadPaiementWeChat> DemarrerPaiement(string description, string refCommandeMarchand, int montantTotal)
        {
            var requete = new PayloadPaiementWeChat();
            requete.DefinirParametre("body", description);
            requete.DefinirParametre("out_trade_no", refCommandeMarchand);
            requete.DefinirParametre("total_fee", montantTotal);
            requete.DefinirParametre("time_start", DateTime.Now.ToString("yyyyMMddHHmmss"));
            requete.DefinirParametre("time_expire", DateTime.Now.AddMinutes(10).ToString("yyyyMMddHHmmss"));
            requete.DefinirParametre("trade_type", "APP"); // Type de transaction pour l'application

            var resultat = await ClientApiWeChatPay.ExecuterCommandeUnifiee(requete);
            return resultat;
        }

        /// <summary>
        /// Génère la seconde signature requise pour le SDK WeChat sur les applications mobiles.
        /// </summary<
        /// <param name="prepayJson">JSON des paramètres de pré-paiement reçus de l'API unifiée.</param>
        /// <returns>JSON contenant les paramètres signés pour le client mobile.</returns>
        public string GenererSignatureClientApp(string prepayJson)
        {
            var dic = JsonConvert.DeserializeObject<Dictionary<string, string>>(prepayJson);
            var donneesSignature = new PayloadPaiementWeChat();
            donneesSignature.DefinirParametre("appid", dic["appid"]);
            donneesSignature.DefinirParametre("partnerid", dic["mch_id"]); // partnerid est mch_id
            donneesSignature.DefinirParametre("prepayid", dic["prepay_id"]);
            donneesSignature.DefinirParametre("package", "Sign=WXPay");
            donneesSignature.DefinirParametre("noncestr", ClientApiWeChatPay.GenererChaineAleatoire());
            donneesSignature.DefinirParametre("timestamp", ClientApiWeChatPay.GenererHorodatage());
            donneesSignature.DefinirParametre("sign", donneesSignature.GenererSignature());
            return donneesSignature.VersJson();
        }
    }
}

3.2. Paiement JSAPI (Compte Officiel WeChat) (GestionnairePaiementJsApi)

Ce gestionnaire facilite le processus de paiement JSAPI, typiquement utilisé dans les navigateurs intégrés à WeChat (Comptes Officiels).

using System;
using System.Threading.Tasks;

namespace Qwerty.Payment.WeChatPay
{
    internal class GestionnairePaiementJsApi
    {
        private PayloadPaiementWeChat _resultatCommandeUnifiee;

        /// <summary>
        /// Appelle l'API de commande unifiée et stocke le résultat.
        /// </summary>
        /// <param name="description">Description du produit.</param>
        /// <param name="refCommandeMarchand">Numéro de commande du marchand.</param>
        /// <param name="montantTotal">Montant total de la commande en cents.</param>
        /// <param name="openId">OpenID de l'utilisateur WeChat.</param>
        /// <returns>Les données de la réponse de la commande unifiée.</returns>
        /// <exception cref="System.Exception">Lancée si la réponse est invalide.</exception>
        public async Task<PayloadPaiementWeChat> ObtenirResultatCommandeUnifiee(string description, string refCommandeMarchand, int montantTotal, string openId)
        {
            var requete = new PayloadPaiementWeChat();
            requete.DefinirParametre("body", description);
            requete.DefinirParametre("out_trade_no", refCommandeMarchand);
            requete.DefinirParametre("total_fee", montantTotal);
            requete.DefinirParametre("time_start", DateTime.Now.ToString("yyyyMMddHHmmss"));
            requete.DefinirParametre("time_expire", DateTime.Now.AddMinutes(10).ToString("yyyyMMddHHmmss"));
            requete.DefinirParametre("trade_type", "JSAPI");
            requete.DefinirParametre("openid", openId);

            var resultat = await ClientApiWeChatPay.ExecuterCommandeUnifiee(requete);
            if (!resultat.ContientParametre("appid") || !resultat.ContientParametre("prepay_id") ||
                string.IsNullOrEmpty(resultat.ObtenirParametre("prepay_id").ToString()))
            {
                throw new System.Exception("La réponse de l'API de commande unifiée pour JSAPI est invalide !");
            }

            _resultatCommandeUnifiee = resultat;
            return resultat;
        }

        /// <summary>
        /// Génère les paramètres requis pour l'invocation du SDK JSAPI WeChat sur la page web.
        /// </summary<
        /// <returns>Une chaîne JSON contenant les paramètres pour le client web.</returns>
        public string ObtenirParametresJsApi()
        {
            var donneesJsApi = new PayloadPaiementWeChat();
            donneesJsApi.DefinirParametre("appId", _resultatCommandeUnifiee.ObtenirParametre("appid"));
            donneesJsApi.DefinirParametre("timeStamp", ClientApiWeChatPay.GenererHorodatage());
            donneesJsApi.DefinirParametre("nonceStr", ClientApiWeChatPay.GenererChaineAleatoire());
            donneesJsApi.DefinirParametre("package", "prepay_id=" + _resultatCommandeUnifiee.ObtenirParametre("prepay_id"));
            donneesJsApi.DefinirParametre("signType", "MD5");
            donneesJsApi.DefinirParametre("paySign", donneesJsApi.GenererSignature());

            return donneesJsApi.VersJson();
        }
    }
}

3.3. Paiement H5 (Navigateur Mobile Externe) (GestionnairePaiementH5)

Ce gestionnaire gère les paiements initiés depuis des navigateurs mobiles en dehors de l'écosystème WeChat.

using System;
using System.Threading.Tasks;

namespace Qwerty.Payment.WeChatPay
{
    internal class GestionnairePaiementH5
    {
        /// <summary>
        /// Obtient l'URL de paiement pour les transactions H5.
        /// </summary>
        /// <param name="description">Description du produit.</param>
        /// <param name="refCommandeMarchand">Numéro de commande du marchand.</param>
        /// <param name="montantTotal">Montant total de la commande en cents.</param>
        /// <param name="ipClient">Adresse IP du client.</param>
        /// <returns>Les données de la réponse de la commande unifiée, contenant l'URL H5.</returns>
        public async Task<PayloadPaiementWeChat> ObtenirUrlPaiement(string description, string refCommandeMarchand, int montantTotal, string ipClient)
        {
            var requete = new PayloadPaiementWeChat();
            requete.DefinirParametre("body", description);
            requete.DefinirParametre("out_trade_no", refCommandeMarchand);
            requete.DefinirParametre("total_fee", montantTotal);
            requete.DefinirParametre("time_start", DateTime.Now.ToString("yyyyMMddHHmmss"));
            requete.DefinirParametre("time_expire", DateTime.Now.AddMinutes(10).ToString("yyyyMMddHHmmss"));
            requete.DefinirParametre("trade_type", "MWEB"); // Type de transaction pour H5
            requete.DefinirParametre("spbill_create_ip", ipClient);

            var resultat = await ClientApiWeChatPay.ExecuterCommandeUnifiee(requete);
            return resultat;
        }
    }
}

3.4. Paiement par Code QR Natif (GestionnairePaiementScan)

Cette méthode est utilisée pour générer des codes QR que les utilisateurs peuvent scanner avec l'application WeChat pour payer. Le mode 2 (commande unifiée) est le plus couramment utilisé.

using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;

namespace Qwerty.Payment.WeChatPay
{
    internal class GestionnairePaiementScan
    {
        /// <summary>
        /// Génère une URL de paiement (Mode 1) qui, une fois scannée, renvoie au serveur du marchand.
        /// Le mode 1 est moins courant et nécessite une implémentation plus complexe côté serveur.
        /// </summary>
        /// <param name="idProduit">ID du produit.</param>
        /// <returns>L'URL du pré-paiement pour le mode 1.</returns>
        public string GenererUrlPrePaiement(string idProduit)
        {
            var donnees = new PayloadPaiementWeChat();
            donnees.DefinirParametre("appid", ConfigurationPaiementWeChat.AppId);
            donnees.DefinirParametre("mch_id", ConfigurationPaiementWeChat.MarchandId);
            donnees.DefinirParametre("time_stamp", ClientApiWeChatPay.GenererHorodatage());
            donnees.DefinirParametre("nonce_str", ClientApiWeChatPay.GenererChaineAleatoire());
            donnees.DefinirParametre("product_id", idProduit);
            donnees.DefinirParametre("sign", donnees.GenererSignature());
            var paramsUrl = ConvertirEnChaineParams(donnees.ObtenirTousLesParametres());
            return $"weixin://wxpay/bizpayurl?{paramsUrl}";
        }

        /// <summary>
        /// Génère l'URL de paiement (Mode 2) à partir de la commande unifiée, qui est directement un code QR.
        /// </summary>
        /// <param name="description">Description du produit.</param>
        /// <param name="refCommandeMarchand">Numéro de commande du marchand.</param>
        /// <param name="montantTotal">Montant total de la commande en cents.</param>
        /// <param name="idProduit">ID du produit.</param>
        /// <returns>L'URL du code QR pour le paiement.</returns>
        public async Task<string> GenererUrlCodeQr(string description, string refCommandeMarchand, int montantTotal, string idProduit)
        {
            var requete = new PayloadPaiementWeChat();
            requete.DefinirParametre("body", description);
            requete.DefinirParametre("out_trade_no", refCommandeMarchand);
            requete.DefinirParametre("total_fee", montantTotal);
            requete.DefinirParametre("time_start", DateTime.Now.ToString("yyyyMMddHHmmss"));
            requete.DefinirParametre("time_expire", DateTime.Now.AddMinutes(10).ToString("yyyyMMddHHmmss"));
            requete.DefinirParametre("trade_type", "NATIVE"); // Type de transaction natif
            requete.DefinirParametre("product_id", idProduit);

            var resultat = await ClientApiWeChatPay.ExecuterCommandeUnifiee(requete);
            string urlCode = resultat.ObtenirParametre("code_url").ToString();
            return urlCode;
        }

        /// <summary>
        /// Convertit un dictionnaire de paramètres en une chaîne au format URL.
        /// </summary>
        /// <param name="parametres">Dictionnaire de paramètres.</param>
        /// <returns>La chaîne d'URL formatée.</returns>
        private string ConvertirEnChaineParams(SortedDictionary<string, object> parametres)
        {
            var builder = new StringBuilder();
            foreach (var paire in parametres)
            {
                builder.Append($"{paire.Key}={paire.Value}&");
            }
            return builder.ToString().TrimEnd('&');
        }
    }
}

4. Définition de l'Interface de Service (IServicePaiementWeChat)

Cette interface définit les opérations publiques disponibles pour l'intégration de WeChat Pay.

using System;
using System.IO;
using System.Threading.Tasks;

namespace Qwerty.Payment.WeChatPay
{
    // Définitions des objets de réponse simplifiés pour l'exemple
    public class ReponseRemboursementWeChat
    {
        public string return_code { get; set; }
        public string return_msg { get; set; }
        public string result_code { get; set; }
        public string transaction_id { get; set; }
        public string out_trade_no { get; set; }
        public string out_refund_no { get; set; }
        public string refund_id { get; set; }
        // Ajoutez d'autres champs si nécessaire
    }

    public class ReponseVerificationRemboursementWeChat
    {
        public string return_code { get; set; }
        public string return_msg { get; set; }
        public string result_code { get; set; }
        public string refund_status_0 { get; set; } // Statut du premier remboursement, si plusieurs
        // Ajoutez d'autres champs si nécessaire
    }

    public class RequeteNotificationWeChat
    {
        public bool EstVerifiee { get; set; }
        public string NumeroPaiementWeChat { get; set; } // transaction_id
        public string NumeroCommandeMarchand { get; set; } // out_trade_no
        public string HeurePaiement { get; set; } // time_end
        public string ContenuNotification { get; set; } // XML brut de la notification
    }

    public interface IServicePaiementWeChat
    {
        /// <summary>
        /// Initialise un paiement par code QR (Mode 2) et retourne les données SVG de l'image.
        /// </summary>
        /// <param name="description">Description du produit.</param>
        /// <param name="refCommandeMarchand">Numéro de commande du marchand.</param>
        /// <param name="montantTotal">Montant total en cents.</param>
        /// <param name="idProduit">ID du produit.</param>
        /// <returns>Tuple (succès, données SVG du QR code ou message d'erreur).</returns>
        Task<Tuple<bool, string>> InitialiserPaiementScan(string description, string refCommandeMarchand, int montantTotal, string idProduit);

        /// <summary>
        /// Initialise un paiement via application mobile et retourne les paramètres signés pour le SDK client.
        /// </summary>
        /// <param name="description">Description du produit.</param>
        /// <param name="refCommandeMarchand">Numéro de commande du marchand.</param>
        /// <param name="montantTotal">Montant total en cents.</param>
        /// <returns>Tuple (succès, JSON des paramètres signés ou message d'erreur).</returns>
        Task<Tuple<bool, string>> InitialiserPaiementApp(string description, string refCommandeMarchand, int montantTotal);

        /// <summary>
        /// Initialise un paiement JSAPI pour les Comptes Officiels WeChat.
        /// </summary>
        /// <param name="description">Description du produit.</param>
        /// <param name="refCommandeMarchand">Numéro de commande du marchand.</param>
        /// <param name="montantTotal">Montant total en cents.</param>
        /// <param name="openIdUtilisateur">OpenID de l'utilisateur.</param>
        /// <returns>Tuple (succès, JSON des paramètres JSAPI ou message d'erreur).</returns>
        Task<Tuple<bool, string>> InitialiserPaiementJsApi(string description, string refCommandeMarchand, int montantTotal, string openIdUtilisateur);

        /// <summary>
        /// Initialise un paiement H5 (via navigateur mobile externe).
        /// </summary>
        /// <param name="description">Description du produit.</param>
        /// <param name="refCommandeMarchand">Numéro de commande du marchand.</param>
        /// <param name="montantTotal">Montant total en cents.</param>
        /// <param name="ipClient">Adresse IP du client.</param>
        /// <returns>Tuple (succès, URL de redirection H5 ou message d'erreur).</returns>
        Task<Tuple<bool, string>> InitialiserPaiementH5(string description, string refCommandeMarchand, int montantTotal, string ipClient);

        /// <summary>
        /// Envoie une demande de remboursement à WeChat Pay.
        /// </summary>
        /// <param name="idTransactionWeChat">ID de transaction WeChat.</param>
        /// <param name="refCommandeMarchand">Numéro de commande du marchand.</param>
        /// <param name="montantTotal">Montant total de la commande originale.</param>
        /// <param name="montantRemboursement">Montant à rembourser.</param>
        /// <param name="refRemboursementMarchand">Numéro de remboursement du marchand.</param>
        /// <returns>L'objet réponse du remboursement.</returns>
        Task<ReponseRemboursementWeChat> DemanderRemboursement(string idTransactionWeChat, string refCommandeMarchand, int montantTotal, int montantRemboursement, string refRemboursementMarchand);

        /// <summary>
        /// Vérifie le statut d'un remboursement.
        /// </summary>
        /// <param name="idRemboursementWeChat">ID de remboursement WeChat.</param>
        /// <param name="refRemboursementMarchand">Numéro de remboursement du marchand.</param>
        /// <param name="idTransactionWeChat">ID de transaction WeChat.</param>
        /// <param name="refCommandeMarchand">Numéro de commande du marchand.</param>
        /// <returns>L'objet réponse de la vérification de remboursement.</returns>
        Task<ReponseVerificationRemboursementWeChat> VerifierRemboursement(string idRemboursementWeChat, string refRemboursementMarchand, string idTransactionWeChat, string refCommandeMarchand);

        /// <summary>
        /// Traite les notifications de paiement asynchrones de WeChat Pay.
        /// </summary>
        /// <param name="donneesNotification">Flux de données XML de la notification.</param>
        /// <returns>Un objet encapsulant le résultat de la notification.</returns>
        Task<RequeteNotificationWeChat> GererNotificationPaiement(Stream donneesNotification);
    }
}

5. Implémentation du Service de Paiement (ServicePaiementWeChat)

Cette classe concrétise les méthodes définies dans l'interface, orchestrant les appels aux classes API et aux gestionnaires de paiement.

using System;
using System.IO;
using System.Text;
using System.Threading.Tasks;
using Newtonsoft.Json;
using ZXing; // Assurez-vous d'avoir le package NuGet ZXing.Net
using ZXing.QrCode.Internal;
using ZXing.QrCode; // Pour QrCodeEncodingOptions

namespace Qwerty.Payment.WeChatPay
{
    public class ServicePaiementWeChat : IServicePaiementWeChat
    {
        // Une méthode de configuration est souvent appelée au démarrage de l'application
        // ConfigurationPaiementWeChat.Initialiser("votreAppId", "votreMchId", "votreCleApi", "cheminCertificat", "motDePasseCertificat");

        public async Task<Tuple<bool, string>> InitialiserPaiementScan(string description, string refCommandeMarchand, int montantTotal, string idProduit)
        {
            try
            {
                var gestionnaireScan = new GestionnairePaiementScan();
                // Génère l'URL pour le paiement par code QR (Mode 2)
                var urlCodeQr = await gestionnaireScan.GenererUrlCodeQr(description, refCommandeMarchand, montantTotal, idProduit);

                // Génère l'image SVG du code QR
                var ecrivainSvg = new BarcodeWriterSvg
                {
                    Format = BarcodeFormat.QR_CODE,
                    Options = new QrCodeEncodingOptions
                    {
                        ErrorCorrection = ErrorCorrectionLevel.H,
                        Width = 300,  // Spécifier la largeur
                        Height = 300 // Spécifier la hauteur
                    }
                };
                var donneesSvg = ecrivainSvg.Write(urlCodeQr);
                return donneesSvg == null
                    ? new Tuple<bool, string>(false, "La génération de l'image du code QR a échoué.")
                    : new Tuple<bool, string>(true, donneesSvg.ToString());
            }
            catch (Exception ex)
            {
                return new Tuple<bool, string>(false, $"Erreur lors de l'initialisation du paiement par scan : {ex.Message}");
            }
        }

        public async Task<Tuple<bool, string>> InitialiserPaiementApp(string description, string refCommandeMarchand, int montantTotal)
        {
            try
            {
                var gestionnaireApp = new GestionnairePaiementApp();
                var resultatCommandeUnifiee = await gestionnaireApp.DemarrerPaiement(description, refCommandeMarchand, montantTotal);

                if (resultatCommandeUnifiee == null ||
                    !resultatCommandeUnifiee.ContientParametre("return_code") ||
                    resultatCommandeUnifiee.ObtenirParametre("return_code").ToString() != "SUCCESS")
                {
                    return new Tuple<bool, string>(false, resultatCommandeUnifiee?.ObtenirParametre("return_msg")?.ToString() ?? "Réponse de paiement App vide ou échouée.");
                }

                // Effectuer la seconde signature pour le SDK de l'application cliente
                string jsonParamsApp = gestionnaireApp.GenererSignatureClientApp(resultatCommandeUnifiee.VersJson());
                return new Tuple<bool, string>(true, jsonParamsApp);
            }
            catch (Exception ex)
            {
                return new Tuple<bool, string>(false, $"Erreur lors de l'initialisation du paiement App : {ex.Message}");
            }
        }

        public async Task<Tuple<bool, string>> InitialiserPaiementJsApi(string description, string refCommandeMarchand, int montantTotal, string openIdUtilisateur)
        {
            try
            {
                var gestionnaireJsApi = new GestionnairePaiementJsApi();
                await gestionnaireJsApi.ObtenirResultatCommandeUnifiee(description, refCommandeMarchand, montantTotal, openIdUtilisateur);
                string paramsJsApi = gestionnaireJsApi.ObtenirParametresJsApi(); // Obtient les paramètres pour l'appel JS API
                return new Tuple<bool, string>(true, paramsJsApi);
            }
            catch (Exception ex)
            {
                return new Tuple<bool, string>(false, $"Erreur lors de l'initialisation du paiement JSAPI : {ex.Message}");
            }
        }

        public async Task<Tuple<bool, string>> InitialiserPaiementH5(string description, string refCommandeMarchand, int montantTotal, string ipClient)
        {
            try
            {
                var gestionnaireH5 = new GestionnairePaiementH5();
                var resultatCommandeUnifiee = await gestionnaireH5.ObtenirUrlPaiement(description, refCommandeMarchand, montantTotal, ipClient);

                if (resultatCommandeUnifiee == null ||
                    !resultatCommandeUnifiee.ContientParametre("return_code") ||
                    resultatCommandeUnifiee.ObtenirParametre("return_code").ToString() != "SUCCESS" ||
                    !resultatCommandeUnifiee.ContientParametre("mweb_url"))
                {
                    return new Tuple<bool, string>(false, resultatCommandeUnifiee?.ObtenirParametre("return_msg")?.ToString() ?? "Réponse de paiement H5 vide ou URL mweb_url manquante.");
                }
                return new Tuple<bool, string>(true, resultatCommandeUnifiee.ObtenirParametre("mweb_url").ToString());
            }
            catch (Exception ex)
            {
                return new Tuple<bool, string>(false, $"Erreur lors de l'initialisation du paiement H5 : {ex.Message}");
            }
        }

        public async Task<ReponseRemboursementWeChat> DemanderRemboursement(string idTransactionWeChat, string refCommandeMarchand, int montantTotal, int montantRemboursement, string refRemboursementMarchand)
        {
            try
            {
                var requete = new PayloadPaiementWeChat();
                requete.DefinirParametre("transaction_id", idTransactionWeChat);
                requete.DefinirParametre("out_trade_no", refCommandeMarchand);
                requete.DefinirParametre("total_fee", montantTotal);
                requete.DefinirParametre("refund_fee", montantRemboursement);
                requete.DefinirParametre("out_refund_no", refRemboursementMarchand);
                requete.DefinirParametre("op_user_id", ConfigurationPaiementWeChat.MarchandId); // Généralement l'ID du marchand

                var resultat = await ClientApiWeChatPay.DemanderRemboursement(requete);
                return JsonConvert.DeserializeObject<ReponseRemboursementWeChat>(resultat.VersJson());
            }
            catch (Exception ex)
            {
                return new ReponseRemboursementWeChat { result_code = "FAIL", return_msg = ex.Message };
            }
        }

        public async Task<ReponseVerificationRemboursementWeChat> VerifierRemboursement(string idRemboursementWeChat, string refRemboursementMarchand, string idTransactionWeChat, string refCommandeMarchand)
        {
            try
            {
                var requete = new PayloadPaiementWeChat();
                if (!string.IsNullOrEmpty(idRemboursementWeChat)) requete.DefinirParametre("refund_id", idRemboursementWeChat);
                if (!string.IsNullOrEmpty(refRemboursementMarchand)) requete.DefinirParametre("out_refund_no", refRemboursementMarchand);
                if (!string.IsNullOrEmpty(idTransactionWeChat)) requete.DefinirParametre("transaction_id", idTransactionWeChat);
                if (!string.IsNullOrEmpty(refCommandeMarchand)) requete.DefinirParametre("out_trade_no", refCommandeMarchand);

                var resultat = await ClientApiWeChatPay.RequerirRemboursement(requete);
                return JsonConvert.DeserializeObject<ReponseVerificationRemboursementWeChat>(resultat.VersJson());
            }
            catch (Exception ex)
            {
                return new ReponseVerificationRemboursementWeChat { result_code = "FAIL", return_msg = ex.Message };
            }
        }

        public async Task<RequeteNotificationWeChat> GererNotificationPaiement(Stream donneesNotification)
        {
            var notification = new RequeteNotificationWeChat { EstVerifiee = false };
            var contenuNotification = new StringBuilder();

            try
            {
                // Lecture du flux de notification
                using (var reader = new StreamReader(donneesNotification, Encoding.UTF8))
                {
                    contenuNotification.Append(await reader.ReadToEndAsync());
                }
                notification.ContenuNotification = contenuNotification.ToString();

                var donneesWxPay = new PayloadPaiementWeChat();
                donneesWxPay.DepuisXml(notification.ContenuNotification); // Inclut la vérification de signature

                // Vérification de la transaction
                if (!donneesWxPay.ContientParametre("transaction_id"))
                {
                    notification.ContenuNotification = new PayloadPaiementWeChat {
                        {"return_code", "FAIL"},
                        {"return_msg", "ID de transaction manquant"}
                    }.VersXml();
                    return notification;
                }

                string transactionId = donneesWxPay.ObtenirParametre("transaction_id").ToString();
                string outTradeNo = donneesWxPay.ObtenirParametre("out_trade_no").ToString();
                string timeEnd = donneesWxPay.ObtenirParametre("time_end").ToString();

                // Validation de la commande côté WeChat Pay pour s'assurer de son authenticité
                if (!await VerifierCommandeWeChat(transactionId))
                {
                    notification.ContenuNotification = new PayloadPaiementWeChat {
                        {"return_code", "FAIL"},
                        {"return_msg", "Vérification de commande échouée"}
                    }.VersXml();
                    return notification;
                }

                // Paiement validé
                notification.EstVerifiee = true;
                notification.NumeroPaiementWeChat = transactionId;
                notification.NumeroCommandeMarchand = outTradeNo;
                notification.HeurePaiement = timeEnd;
                notification.ContenuNotification = new PayloadPaiementWeChat {
                    {"return_code", "SUCCESS"},
                    {"return_msg", "OK"}
                }.VersXml();

                return notification;
            }
            catch (Exception ex)
            {
                notification.ContenuNotification = new PayloadPaiementWeChat {
                    {"return_code", "FAIL"},
                    {"return_msg", $"Erreur de traitement de notification : {ex.Message}"}
                }.VersXml();
                return notification;
            }
        }

        /// <summary>
        /// Interne : Vérifie l'état d'une commande via l'API WeChat Pay.
        /// </summary>
        /// <param name="transactionId">ID de transaction WeChat.</param>
        /// <returns>True si la commande est réussie, sinon False.</returns>
        private static async Task<bool> VerifierCommandeWeChat(string transactionId)
        {
            var requete = new PayloadPaiementWeChat();
            requete.DefinirParametre("transaction_id", transactionId);
            var reponse = await ClientApiWeChatPay.RequerirCommande(requete);
            return reponse.ContientParametre("return_code") && reponse.ObtenirParametre("return_code").ToString() == "SUCCESS" &&
                   reponse.ContientParametre("result_code") && reponse.ObtenirParametre("result_code").ToString() == "SUCCESS";
        }
    }
}

6. Services Utilitaires et Configuration

6.1. Client HTTP Sécurisé (ClientHttpWeChat)

Cette classe gère les requêtes HTTP, avec une prise en charge spécifique des certificats pour les API sécurisées comme le remboursement.

using System.Net.Http;
using System.Security.Authentication;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Threading.Tasks;

namespace Qwerty.Payment.WeChatPay
{
    internal static class ClientHttpWeChat
    {
        /// <summary>
        /// Envoie une requête POST avec un payload XML à une URL donnée.
        /// </summary>
        /// <param name="xmlPayload">Contenu XML de la requête.</param>
        /// <param name="url">URL de destination.</param>
        /// <param name="utiliserCertificat">Indique si un certificat client doit être utilisé.</param>
        /// <param name="delaiExpiration">Délai d'expiration de la requête en secondes.</param>
        /// <returns>La réponse de l'API sous forme de chaîne XML.</returns>
        public static async Task<string> EnvoyerPost(string xmlPayload, string url, bool utiliserCertificat, int delaiExpiration)
        {
            HttpClient client;

            if (utiliserCertificat)
            {
                // Chargement du certificat PFX pour l'authentification client
                var certificatClient = new X509Certificate2(
                    ConfigurationPaiementWeChat.CheminCertificatSsl,
                    ConfigurationPaiementWeChat.MotDePasseCertificatSsl
                );

                var gestionnaireHttp = new HttpClientHandler
                {
                    ClientCertificateOptions = ClientCertificateOption.Manual,
                    SslProtocols = SslProtocols.Tls12 | SslProtocols.Tls11 | SslProtocols.Tls // Compatible avec différents niveaux TLS
                };
                gestionnaireHttp.ClientCertificates.Add(certificatClient);

                // Ignore les erreurs de validation de certificat serveur (souvent nécessaire pour certaines APIs)
                gestionnaireHttp.ServerCertificateCustomValidationCallback = (message, cert, chain, errors) => true;

                client = new HttpClient(gestionnaireHttp);
            }
            else
            {
                client = new HttpClient();
            }

            client.Timeout = TimeSpan.FromSeconds(delaiExpiration);
            var contenuHttp = new StringContent(xmlPayload, Encoding.UTF8, "application/xml");
            
            using (client)
            {
                var reponse = await client.PostAsync(url, contenuHttp);
                reponse.EnsureSuccessStatusCode(); // Lance une exception pour les codes d'erreur HTTP
                return await reponse.Content.ReadAsStringAsync();
            }
        }
    }
}

6.2. Configuration de WeChat Pay (ConfigurationPaiementWeChat)

Cette classe statique contient les paramètres de configuration globaux nécessaires pour interagir avec l'API WeChat Pay. Il est crucial de configurer ces valeurs au démarrage de votre application.

namespace Qwerty.Payment.WeChatPay
{
    /// <summary>
    /// Configure les informations de compte WeChat Pay.
    /// </summary>
    internal static class ConfigurationPaiementWeChat
    {
        // Propriétés de configuration
        public static string AppId { get; private set; }
        public static string MarchandId { get; private set; } // MCHID
        public static string CleAPI { get; private set; } // KEY
        public static string CheminCertificatSsl { get; private set; }
        public static string MotDePasseCertificatSsl { get; private set; }
        public static string UrlNotification { get; private set; } // URL de callback pour les notifications de paiement
        public static string AdresseIP { get; private set; } // IP du serveur marchand
        public static int NiveauRapport { get; private set; } // Niveau de rapport : 0=désactivé, 1=erreurs seulement, 2=tout

        /// <summary>
        /// Initialise les paramètres de configuration de WeChat Pay.
        /// Cette méthode doit être appelée une seule fois au démarrage de l'application.
        /// </summary>
        public static void Initialiser(string appId, string marchandId, string cleApi, string cheminCertificat, string motDePasseCertificat, string urlNotification, string adresseIp, int niveauRapport = 1)
        {
            AppId = appId;
            MarchandId = marchandId;
            CleAPI = cleApi;
            CheminCertificatSsl = cheminCertificat;
            MotDePasseCertificatSsl = motDePasseCertificat;
            UrlNotification = urlNotification;
            AdresseIP = adresseIp;
            NiveauRapport = niveauRapport;
        }

        // --- Valeurs par défaut ou exemples si non initialisées, à remplacer par votre configuration réelle ---
        // public static string AppId { get; private set; } = "votre_appid";
        // public static string MarchandId { get; private set; } = "votre_mchid";
        // public static string CleAPI { get; private set; } = "votre_cle_api";
        // public static string CheminCertificatSsl { get; private set; } = "path/to/your/apiclient_cert.p12";
        // public static string MotDePasseCertificatSsl { get; private set; } = "votre_mchid"; // Souvent l'ID du marchand
        // public static string UrlNotification { get; private set; } = "https://votredomaine.com/api/wechat/notify";
        // public static string AdresseIP { get; private set; } = "0.0.0.0"; // L'IP publique de votre serveur
        // public static int NiveauRapport { get; private set; } = 1; 
    }
}

Notes Importantes :

  • Les API de remboursement et d'annulation de commande de WeChat Pay exigent une authentification client via certificat SSL (généralement un fichier .p12). Assurez-vous que le chemin du certificat et le mot de passe sont correctement configurés.
  • Selon la nature de votre compte WeChat Pay et les produits que vous proposez, vous pourriez avoir des AppId et MarchandId distincts pour les différents scénarios (par exemple, App, JSAPI, H5). Une gestion flexible de ces configurations est recommandée.

Étiquettes: .NET Core WeChat Pay Intégration de Paiement API REST C#

Publié le 20 août à 10h02