it is usable

parent 0f770ecb
......@@ -20,7 +20,6 @@ Global
{200DFB2F-E0E0-4B38-BE26-12C3E93DE991}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{200DFB2F-E0E0-4B38-BE26-12C3E93DE991}.Debug|Any CPU.Build.0 = Debug|Any CPU
{200DFB2F-E0E0-4B38-BE26-12C3E93DE991}.Release|Any CPU.ActiveCfg = Release|Any CPU
{200DFB2F-E0E0-4B38-BE26-12C3E93DE991}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
......
......@@ -81,8 +81,8 @@ namespace ShoptimizaSalesFileExporter.Tests
.Returns(true)
.Returns(false);
reader.Setup(x => x.FieldCount).Returns(2);
reader.SetupSequence(x => x.GetString(0)).Returns("123").Returns("123").Returns("222");
reader.SetupSequence(x => x.GetString(1)).Returns("P1").Returns("P2").Returns("P3");
reader.SetupSequence(x => x["oid"]).Returns("123").Returns("123").Returns("222");
reader.SetupSequence(x => x["iid"]).Returns("P1").Returns("P2").Returns("P3");
PrivateType privateType = new PrivateType(typeof(MySqlSalesFile));
privateType.InvokeStatic("Reader2SalesFile", reader.Object, file);
......@@ -92,8 +92,8 @@ namespace ShoptimizaSalesFileExporter.Tests
Assert.AreEqual("123", records[0].Key);
CollectionAssert.AreEqual(new[] { "P1", "P2" }, records[0].Items);
Assert.AreEqual("222", records[0].Key);
CollectionAssert.AreEqual(new[] { "P3"}, records[0].Items);
Assert.AreEqual("222", records[1].Key);
CollectionAssert.AreEqual(new[] { "P3"}, records[1].Items);
Assert.AreEqual(3, contents.Length);
......@@ -150,7 +150,7 @@ namespace ShoptimizaSalesFileExporter.Tests
{
var definition = new { Key = "", Items = new[] { "" } };
var selectQuery = "Select * from sales;";
var selectQuery = "Select order_id as oid, product_id as iid from sales;";
MySqlSalesFile salesFile = new MySqlSalesFile(this.connStr, selectQuery);
var filePath = Path.Combine(TestContext.CurrentContext.TestDirectory, "testOutput.txt");
salesFile.BuildSalesFile(file);
......
This diff is collapsed.
'use strict';
const express = require('express'),
fs = require('fs'),
port = 3000,
app = express();
app.all('*' ,function(req, res, next){
console.error('incoming request' , req.originalUrl);
console.error(' headers', req.headers);
return next();
});
app.get('/v1/sites/:apiKey', function (req, res){
res.json({
domain:`http://localhost:${port}/`,
blablabla: "bla"
});
});
function apiDiscovery(req, res, next){
res.json({
services : {
getMappingsByField:`http://localhost:${port}/index.php?route=extension/module/shoptimiza/getMappingsByField&websiteid=$websiteid&posid=$posid`,
getMappingFieldList:`http://localhost:${port}/index.php?route=extension/module/shoptimiza/getMappingFieldList`
}
});
}
function getMappingsByField(req, res, next){
/*
* /index.php?route=extension/module/shoptimiza/getMappingsByField&websiteid=$websiteid&posid=$posid", function (req, res){
*/
return res.json({
mappings:{
"1": "uno",
"2": "dos"
}
});
}
function getMappingFieldList(req, res, next){
return res.json({
fields:["field1", "field2"]
});
}
app.get('/index.php', function (req, res, next){
if(req.query.route === "extension/module/shoptimiza/apiDiscovery"){
console.error(' dispatch apiDiscovery');
return apiDiscovery(req, res, next);
}
if(req.query.route === "extension/module/shoptimiza/getMappingsByField"){
console.error(' dispatch getMappingsByField');
return getMappingsByField(req, res, next);
}
if(req.query.route ==="extension/module/shoptimiza/getMappingFieldList"){
console.error(' dispatch getMappingFieldList');
return getMappingFieldList(req, res, next);
}
});
app.put('*', function (req, res){
var fileId = "uploaded" + Date.now();
var writer = fs.createWriteStream(fileId);
req.pipe(writer);
req.on('end',function(){
res.end();
});
});
app.listen(port, function(err){
if(err){
console.error('Unable to start server');
process.exit(1);
}
console.error(`Test API listening at http://localhost${port}`);
});
{"key":"1","items":["uno"]}
{"key":"2","items":["dos"]}
{"key":"1","items":["uno"]}
{"key":"2","items":["dos"]}
{"key":"1","items":["uno"]}
{"key":"2","items":["dos"]}
{"key":"1","items":["uno"]}
{"key":"2","items":["dos"]}
This diff is collapsed.
This diff is collapsed.
......@@ -132,4 +132,10 @@
<metadata name="testDBQueryWorker.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
<value>583, 17</value>
</metadata>
<metadata name="testMappingUrlWorker.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
<value>743, 17</value>
</metadata>
<metadata name="fetchMappingFieldsWorker.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
<value>920, 17</value>
</metadata>
</root>
\ No newline at end of file
This diff is collapsed.
......@@ -12,13 +12,66 @@ namespace ShoptimizaSalesFileExporter
public partial class MainForm : Form
{
private ConfigurationWizard configurationWizard;
public List<OperationLogEntry> Logs = null;
public OperationLogEntry LastError;
public OperationLogEntry LastSuccess;
public MainForm()
{
InitializeComponent();
}
public void SetLastError(OperationLogEntry log)
{
LastError = log;
if (LastSuccess == null || LastError.When > LastSuccess.When)
{
this.txtExportError.Invoke((MethodInvoker)delegate
{
this.txtExportError.Visible = true;
this.txtExportError.Text = "Woooops! We are having some issues posting your sales data to Shoptimiza. \n\r";
this.txtExportError.Text += "What can you do? Just ignore it for the moment. If the problem persists please send us an email to support@shoptimiza.com\n\r";
this.txtExportError.Text += "---- Details ----\n\r";
this.txtExportError.Text += "When:" + LastError.When.ToString() + "\n\r";
this.txtExportError.Text += "What:" + LastError.Message + "\n\r";
});
}
}
public void SetLastSuccessUpdate(OperationLogEntry log)
{
LastSuccess = log;
this.lblLastExportValue.Invoke((MethodInvoker)delegate
{
if(LastError != null)
{
this.txtExportError.Visible = false;
this.txtExportError.Text = "";
}
this.lblLastExportValue.Text = log.When.ToString();
});
}
private void MainForm_Load(object sender, EventArgs e)
{
this.lblLastExportValue.Text = "Unknown";
Logs = new List<OperationLogEntry>();
if (Configuration.Instance.Load())
{
pnLaunchWizard.Hide();
pnMain.Show();
lblApiKeyValue.Text = Configuration.Instance.APIKey;
lblDomainValue.Text = Configuration.Instance.Domain;
}
else
{
pnLaunchWizard.Show();
pnMain.Hide();
}
Logs.Add(new OperationLogEntry { Success = true, Message = "this is a test" });
Logs.Add(new OperationLogEntry { Success = true, Message = "oh oh" });
var bindingList = new BindingList<OperationLogEntry>(Logs);
var source = new BindingSource(bindingList, null);
}
......@@ -48,7 +101,9 @@ namespace ShoptimizaSalesFileExporter
notifyIcon1.ShowBalloonTip(1000);
notifyIcon1.BalloonTipTitle = "Shoptimiza Sales File Exporter minimized";
this.ShowInTaskbar = false;
}else if(this.WindowState ==FormWindowState.Normal) {
}
else if (this.WindowState == FormWindowState.Normal)
{
notifyIcon1.Visible = false;
}
......
......@@ -3,22 +3,82 @@ using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Windows.Forms;
using System.Reflection;
using System.IO;
using Timer = System.Timers.Timer;
using System.Threading;
namespace ShoptimizaSalesFileExporter
{
static class Program
{
/// <summary>
/// The main entry point for the application.
/// </summary>
[STAThread]
static void Main()
{
bool confExists = Configuration.Instance.Load();
EnsureOnlyOneInstanceOfThisApplication();
bool confExists = Configuration.Instance.Load();
SyncWorker w = new SyncWorker();
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new MainForm());
var mainForm = new MainForm();
int count = 1;
Timer timer = new Timer(10000);
timer.AutoReset = true;
timer.Elapsed += (sender, e) =>
{
var logs = w.Logs.ToArray();
Console.WriteLine("number of logs = " + logs.Length);
var last = logs.OrderByDescending(x => x.When).FirstOrDefault();
if (last != null && last.Success == false)
{
timer.Interval = 60 * 1000; // one minute
Console.WriteLine("Last success is null");
mainForm.SetLastError(last);
}
if(last != null && last.Success)
{
Console.WriteLine("Last success" + last.When.ToString() + " interval changed to " + timer.Interval);
timer.Interval = 24 * 60 * 60 * 1000; // 24H
mainForm.SetLastSuccessUpdate(last);
}
Thread newThread = new Thread(w.RunUpdate);
newThread.IsBackground = true;
newThread.Start(Configuration.Instance);
Console.WriteLine("Elapsed");
};
timer.Start();
Application.Run(mainForm);
}
private static void Timer_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
{
SetupSync();
}
private static void SetupSync()
{
}
static void EnsureOnlyOneInstanceOfThisApplication()
{
var instances = Process.GetProcessesByName(Path.GetFileNameWithoutExtension(Assembly.GetEntryAssembly().Location));
if (instances.Count() > 1)
{
MessageBox.Show("You can only run one instance of this program. Is it on the system tray?");
Environment.Exit(0);
}
}
}
}
<?xml version="1.0" encoding="utf-8"?>
<!--
This file is automatically generated by Visual Studio .Net. It is
used to store generic object data source configuration information.
Renaming the file extension or editing the content of this file may
cause the file to be unrecognizable by the program.
-->
<GenericObjectDataSource DisplayName="OperationLogEntry" Version="1.0" xmlns="urn:schemas-microsoft-com:xml-msdatasource">
<TypeInfo>ShoptimizaSalesFileExporter.OperationLogEntry, ShoptimizaSalesFileExporter, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null</TypeInfo>
</GenericObjectDataSource>
\ No newline at end of file
......@@ -44,13 +44,11 @@
<Reference Include="Newtonsoft.Json, Version=11.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL">
<HintPath>..\packages\Newtonsoft.Json.11.0.2\lib\net40\Newtonsoft.Json.dll</HintPath>
</Reference>
<Reference Include="nunit.framework, Version=3.10.1.0, Culture=neutral, PublicKeyToken=2638cd05610744eb, processorArchitecture=MSIL">
<HintPath>..\packages\NUnit.3.10.1\lib\net40\nunit.framework.dll</HintPath>
</Reference>
<Reference Include="ShoptimizaAuthHeader">
<HintPath>..\..\ShoptimizaAuthHeader\ShoptimizaAuthHeader\bin\Debug\ShoptimizaAuthHeader.dll</HintPath>
</Reference>
<Reference Include="System" />
<Reference Include="System.configuration" />
<Reference Include="System.Core" />
<Reference Include="System.Design" />
<Reference Include="System.Xml.Linq" />
......@@ -73,6 +71,8 @@
<Compile Include="lib\Configuration.cs" />
<Compile Include="lib\MySqlSalesFile.cs" />
<Compile Include="lib\ShoptimizaWebClient.cs" />
<Compile Include="lib\SyncWorker.cs" />
<Compile Include="lib\Util.cs" />
<Compile Include="MainForm.cs">
<SubType>Form</SubType>
</Compile>
......@@ -96,8 +96,12 @@
<AutoGen>True</AutoGen>
<DependentUpon>Resources.resx</DependentUpon>
</Compile>
<None Include="app.config" />
<None Include="app.config">
<SubType>Designer</SubType>
</None>
<None Include="packages.config" />
<None Include="Properties\DataSources\OperationLogEntry.datasource" />
<None Include="Properties\DataSources\SyncWorker.OperationLogEntry.datasource" />
<None Include="Properties\Settings.settings">
<Generator>SettingsSingleFileGenerator</Generator>
<LastGenOutput>Settings.Designer.cs</LastGenOutput>
......
......@@ -6,4 +6,14 @@
<add name="MySQL Data Provider" invariant="MySql.Data.MySqlClient" description=".Net Framework Data Provider for MySQL" type="MySql.Data.MySqlClient.MySqlClientFactory, MySql.Data, Version=6.9.12.0, Culture=neutral, PublicKeyToken=c5687fc88969c44d" />
</DbProviderFactories>
</system.data>
<appSettings>
<add key="ResolveDomainUrl" value="https://api.shoptimiza.com/v1/sites/$apiKey"/>
<add key="PushFileUrl" value="https://api.shoptimiza.com/v1/sites/$apiKey/sales-file"/>
<!-- SalesFileTTL 7 days in seconds-->
<clear />
<add key="SalesFileTTL" value="604800000"/>
<add key="ResolveDomainUrl" value="http://localhost:3000/v1/sites/$apiKey"/>
<add key="PushFileUrl" value="http://localhost:3000/v1/sites/$apiKey/sales-file"/>
<add key="ApiDiscoveryUrl" value ="/index.php?route=extension/module/shoptimiza/apiDiscovery"/>
</appSettings>
</configuration>
\ No newline at end of file
......@@ -13,15 +13,21 @@ namespace ShoptimizaSalesFileExporter
const String FileName = "conf.json";
public string APIKey;
public string Secret;
public string MySqlConnStr;
public string MySqlQuery;
public string Domain;
public string APIDiscoveryUrl;
public string GetMappingsByField;
public string GetMappingFieldList;
public string POSIDMappingField;
public string WebsiteIDMappingField;
private static readonly Lazy<Configuration> lazy =
new Lazy<Configuration>(() => new Configuration());
public static Configuration Instance { get { return lazy.Value; } }
private String GetFilePath()
{
return Path.Combine(Application.StartupPath, Configuration.FileName);
......@@ -37,6 +43,14 @@ namespace ShoptimizaSalesFileExporter
Configuration conf = JsonConvert.DeserializeObject<Configuration>(json);
this.APIKey = conf.APIKey;
this.Secret = conf.Secret;
this.Domain = conf.Domain;
this.MySqlConnStr = conf.MySqlConnStr;
this.MySqlQuery = conf.MySqlQuery;
this.APIDiscoveryUrl = conf.APIDiscoveryUrl;
this.GetMappingFieldList = conf.GetMappingFieldList;
this.GetMappingsByField = conf.GetMappingsByField;
this.POSIDMappingField = conf.POSIDMappingField;
this.WebsiteIDMappingField = conf.WebsiteIDMappingField;
return true;
}
}
......@@ -48,12 +62,18 @@ namespace ShoptimizaSalesFileExporter
}
public void Save()
{
string file = GetFilePath();
JsonSerializer serializer = new JsonSerializer();
using (StreamWriter sw = new StreamWriter(GetFilePath()))
using (JsonWriter writer = new JsonTextWriter(sw))
using (var fs = File.Create(file))
using (var writer = new StreamWriter(fs))
using (var json = new JsonTextWriter(writer))
{
serializer.Serialize(writer, this);
json.Formatting = Formatting.Indented;
serializer.Serialize(json, this);
}
}
}
......
using System;
using System.Collections.Generic;
using System.Data;
using System.IO;
using System.Linq;
using System.Text;
using MySql;
using MySql.Data.MySqlClient;
namespace ShoptimizaSalesFileExporter
{
......@@ -12,27 +12,37 @@ namespace ShoptimizaSalesFileExporter
string connStr;
string query;
Dictionary<string, string> mappings;
public MySqlSalesFile(string connStr, string query)
{
this.connStr = connStr;
this.query = query;
}
public void BuildSalesFile(string path)
public MySqlSalesFile(string connStr, string query, Dictionary<string, string> mappings) : this(connStr, query)
{
this.mappings = mappings;
}
public int BuildSalesFile(string path)
{
int ret;
using (var conn = new MySqlConnection(this.connStr))
{
conn.Open();
using (var cmd = new MySqlCommand(this.query, conn))
using (var reader = cmd.ExecuteReader())
{
Reader2SalesFile(reader, path);
ret = Reader2SalesFile(reader, mappings, path);
}
conn.Close();
}
return ret;
}
private static void Reader2SalesFile(IDataReader reader, string path)
private static int Reader2SalesFile(IDataReader reader, Dictionary<string, string> mappings, string path)
{
int recordsWritten = 0;
using (var fs = new FileStream(path, FileMode.Create))
using (var file = new StreamWriter(fs, Encoding.UTF8))
{
......@@ -40,22 +50,34 @@ namespace ShoptimizaSalesFileExporter
var appender = new Accumulator();
while (reader.Read())
{
var output = appender.Append(reader.GetString(0), reader.GetString(1));
string orderId = reader["oid"].ToString();
string productId = reader["iid"].ToString();
if (mappings != null && false == mappings.TryGetValue(productId, out productId))
{
// using mapping but no value found for this id.
continue;
}
var output = appender.Append(orderId, productId);
if (output != null)
{
recordsWritten++;
file.WriteLine(Record2JSON(output.Item1, output.Item2));
}
}
var lastRecord = appender.Flush();
if (lastRecord != null)
{
recordsWritten++;
file.WriteLine(Record2JSON(lastRecord.Item1, lastRecord.Item2));
}
file.Flush();
file.Close();
if(recordsWritten == 0)
{
File.Delete(path);
}
}
return recordsWritten;
}
private static string Record2JSON(string orderId, string[] items)
......
This diff is collapsed.
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Timers;
using System.Xml;
using MySql.Data;
using System.Xml.Serialization;
using Newtonsoft.Json;
using MySql.Data.MySqlClient;
using System.Data;
using System.Configuration;
namespace ShoptimizaSalesFileExporter
{
public sealed class SyncWorker
{
public List<OperationLogEntry> Logs;
private Object ThisLock = new object();
private bool Working = false;
private string SalesFileUrl;
private static String GetFilePath()
{
return Path.Combine(Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location), "log.xml");
}
private static String GetSalesFilePath()
{
return Path.Combine(Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location), "sales-file.ndj");
}
public SyncWorker()
{
Logs = new List<OperationLogEntry>();
SalesFileUrl = ConfigurationManager.AppSettings["PushFileUrl"];
}
static string GetMappingsByField(Configuration conf)
{
return conf.GetMappingsByField
.Replace("$websiteid", conf.WebsiteIDMappingField)
.Replace("$posid", conf.POSIDMappingField);
}
static Dictionary<string, string> FetchMappings(Configuration conf)
{
var definition = new { Mappings = new Dictionary<string, string>() };
ShoptimizaWebClient client = new ShoptimizaWebClient(conf.APIKey, conf.Secret);
var url = GetMappingsByField(conf);
var responseBody = client.DoGetRequest(url);
if (string.IsNullOrEmpty(responseBody))
{
throw new ShoptimizaUnableToFetchMappings("Server returned empty response");
}
return JsonConvert.DeserializeAnonymousType(responseBody, definition).Mappings;
}
public void RunUpdate(object _conf)
{
Console.Write("Run Update...");
lock (ThisLock)
{
if (Working == true)
{
Console.WriteLine("already running. Exiting");
return;
}
Console.WriteLine("locking!");
Working = true;
}
string salesFilePath = GetSalesFilePath();
Configuration conf = (Configuration)_conf;
MySqlSalesFile salesFile = null;
bool mapRecords = !String.IsNullOrEmpty(conf.POSIDMappingField);
if (mapRecords)
{
try
{
var mappings = FetchMappings(conf);
if (mappings != null)
{
salesFile = new MySqlSalesFile(conf.MySqlConnStr, conf.MySqlQuery, mappings);
}
else
{
salesFile = new MySqlSalesFile(conf.MySqlConnStr, conf.MySqlQuery);
}
}
catch (ShoptimizaTooManyErroredRequests ex)
{
lock (ThisLock)
{
var last = ex.ErrorSummary.Last().Item2;
Logs.Add(new OperationLogEntry { Success = false, Message = "Unable to fetch mappings: " + last });
SaveOperationLog(Logs);
Working = false;
}
return;
}
catch (Exception ex)
{
lock (ThisLock)
{
Logs.Add(new OperationLogEntry { Success = false, Message = "Unable to fetch mappings: " + ex.Message });
SaveOperationLog(Logs);
Working = false;
}
return;
}
}
else
{
salesFile = new MySqlSalesFile(conf.MySqlConnStr, conf.MySqlQuery);
}
var recordsOut = salesFile.BuildSalesFile(salesFilePath);
if (recordsOut == 0)
{
lock (ThisLock)
{
Logs.Add(new OperationLogEntry { Success = false, Message = "No records written" });
SaveOperationLog(Logs);
Working = false;
}
return;
}
ShoptimizaWebClient client = new ShoptimizaWebClient(conf.APIKey, conf.Secret);
var responseBody = "Something went wrong";
try
{
responseBody = client.DoPutRequest(salesFilePath, this.SalesFileUrl);
}
catch (ShoptimizaTooManyErroredRequests ex)
{
lock (ThisLock)
{
var last = ex.ErrorSummary.Last().Item2;
Logs.Add(new OperationLogEntry { Success = false, Message = "Unable to put sales file: " + last });
SaveOperationLog(Logs);
Working = false;
}
return;
}
catch (Exception ex)
{
lock (ThisLock)
{
Logs.Add(new OperationLogEntry { Success = false, Message = "Unable to put sales file: " + ex.Message });
SaveOperationLog(Logs);
Working = false;
}
return;
}
lock (ThisLock)
{
Logs.Add(new OperationLogEntry { Success = true, Message = responseBody });
SaveOperationLog(Logs);
Working = false;
}
}
static OperationLogEntry GetLastSuccessEntry(IEnumerable<OperationLogEntry> input)
{
return input.OrderBy(x => x.When).First(x => x.Success);
}
public static OperationLogEntry[] LoadOperationLogs()
{
string path = GetFilePath();
var serializer = new XmlSerializer(typeof(OperationLogEntry[]), new XmlRootAttribute("logs"));
using (var fs = new FileStream(path, FileMode.Open))
{
try
{
return (OperationLogEntry[])serializer.Deserialize(fs);
}
catch (Exception ex)
{
return null;
}
}
}
public void SaveOperationLog(IEnumerable<OperationLogEntry> input)