Hammas
Meeskond
- Sten Lunden
Projekti kirjeldus
E-poe lahendus hambaraviseadmeid ja -tehnikat müüvale ettevõttele. Eesmärk on ehitada võimalikult õhuke e-kommertskiht mis vastaks minimaalsetele ärilistele nõudmistele. Samas peaks lahenduse arhitektuur olema võimalikult modulaarne, võimaldades lisaarendusi tulevikus.
Minimaalsed ärilised nõudmised
Poe avalik osa (storefront)
- Ladustatud toodete kuvamine
- Otsing
- Tootegrupi, hankija filtreering
- Toodete variatsioonid
- Hinnakujundus
- Ostukorvihaldus
- Omniva ja SmartPosti pakiautomaatide valikud transpordimeetodites
- Integratsioon Maksekeskuse API'ga
- Kasutajate autoriseerimine, konto, ostuajalugu
- Lokaliseeritud EE\RU\EN
- SEO
Poe haldusosa (backend)
- Hinnakirja, hankijate, tootegruppide haldus (CRUD)
- Müügikampaaniate haldus
- Toote, hankija, tootegrupi tasemel
- Protsentuaalne ja fikseeritud allahindlus
- Kampaania kestus
- Kampaania sihtgrupp (sisseloginud või VIP kliendid jne)
- Dashboard (nice to have)
- Viimased tellimused
- Registreerunud kasutajad
- ...
Skoobist (momendil) väljas
- Responsive design
- Väljatrükid (arve, tellimus) ja aruandlus ==> SSRS(?)
- Tootepildid
Tehniline baas
- ASP.NET Core 2.0 (API)
- AngularJS (klient)
- IdentityServer 4
- Azure hosting
Andmemudel
Põhilised kasutusjuhud
- Lõppklient
- Ligipääs ainult storefrondile
- Soovi korral autendib ennast
- Otsib tooteid
- Lisab ja eemaldab tooteid oma ostukorvist
- Viib ostuprotsessi lõpuni:
- ..valides transpordimeetodi
- ..valides maksemeetodi
- Kui kasutaja on autenditud jääb ostusündmus kasutaja ajalukku
- Administraator
- Ligipääs poe halduslehele
- Saab luua uusi tooteid ja toodetega seotud olemeid
- Saab hõlpsasti navigeerida hinnakirjas ja seda muuta (in-line edit)
- Saab defineerida müügikampaaniaid ja ad-hoc allahindlusi
- Omab ülevaadet põhilistest ärilistest indikaatoritest
- Tellimuste arv
- Klientide arv
- ...
XML
Andmefail
<?xml version="1.0" encoding="utf-8"?>
<Products>
<Product id="727" parentId="600">
<Name>
<et-EE><![CDATA[TWINKY STAR refill, roheline 5cm 10g]]></et-EE>
<en-GB><![CDATA[TWINKY STAR refill, green 5cm 10g]]></en-GB>
<fi-FI><![CDATA[TWINKY STAR refill, vihreä 5cm 10g]]></fi-FI>
</Name>
<Prices Sales="860.00" Special="791.20" />
<Properties>
<Property>
<Type><![CDATA[Värv]]></Type>
<Value><![CDATA[Roheline]]></Value>
</Property>
<Property>
<Type><![CDATA[Läbimõõt]]></Type>
<Value><![CDATA[5cm]]></Value>
</Property>
<Property>
<Type><![CDATA[Kaal]]></Type>
<Value><![CDATA[10g]]></Value>
</Property>
</Properties>
<Availability>
<Stock id="1">
<Name><![CDATA[Maneezi]]></Name>
<AvailableQty>25.00</AvailableQty>
</Stock>
<Stock id="2">
<Name><![CDATA[Hansakeskus]]></Name>
<AvailableQty>19.00</AvailableQty>
</Stock>
</Availability>
</Product>
<Product id="231">
<Name>
<et-EE><![CDATA[ENDO BOX Dia-Dent 60+4pesa Medium ümar]]></et-EE>
</Name>
<Prices Sales="17.50" />
<Properties />
<Availability>
<Stock id="2">
<Name><![CDATA[Hansakeskus]]></Name>
<AvailableQty>3.00</AvailableQty>
</Stock>
</Availability>
</Product>
<Product id="190">
<Name>
<et-EE><![CDATA[DENTIRO Light 5l]]></et-EE>
<ru-RU><![CDATA[Light 5л]]></ru-RU>
</Name>
<Prices Sales="24.50" Special="14.95" />
<Availability>
<Stock id="1">
<Name><![CDATA[Maneezi]]></Name>
<AvailableQty>114.00</AvailableQty>
</Stock>
<Stock id="2">
<Name><![CDATA[Hansakeskus]]></Name>
<AvailableQty>223.00</AvailableQty>
</Stock>
</Availability>
<Properties>
<Property>
<Type><![CDATA[Varjund]]></Type>
<Value><![CDATA[A4]]></Value>
</Property>
</Properties>
</Product>
</Products>
Skeemifail
<?xml version="1.0" encoding="utf-8"?>
<xs:schema attributeFormDefault="unqualified" elementFormDefault="qualified" xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:element name="Products">
<xs:complexType>
<xs:sequence>
<xs:element maxOccurs="unbounded" name="Product">
<xs:complexType>
<xs:sequence>
<xs:choice maxOccurs="unbounded">
<xs:element name="Name">
<xs:complexType>
<xs:sequence>
<xs:element name="et-EE" type="xs:string" />
<xs:element minOccurs="0" name="ru-RU" type="xs:string" />
<xs:element minOccurs="0" name="en-GB" type="xs:string" />
<xs:element minOccurs="0" name="fi-FI" type="xs:string" />
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:element name="Prices">
<xs:complexType>
<xs:attribute name="Sales" type="xs:decimal" use="required" />
<xs:attribute name="Special" type="xs:decimal" use="optional" />
</xs:complexType>
</xs:element>
<xs:element name="Properties">
<xs:complexType>
<xs:sequence minOccurs="0">
<xs:element maxOccurs="unbounded" name="Property">
<xs:complexType>
<xs:sequence>
<xs:element name="Type" type="xs:string" />
<xs:element name="Value" type="xs:string" />
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:element name="Availability">
<xs:complexType>
<xs:sequence>
<xs:element maxOccurs="unbounded" name="Stock">
<xs:complexType>
<xs:sequence>
<xs:element name="Name" type="xs:string" />
<xs:element name="AvailableQty" type="xs:decimal" />
</xs:sequence>
<xs:attribute name="id" type="xs:int" use="required" />
</xs:complexType>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:choice>
</xs:sequence>
<xs:attribute name="id" type="xs:int" use="required" />
<xs:attribute name="parentId" type="xs:int" use="optional" />
</xs:complexType>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:schema>
Transformatsioon HTMLi
Transformatsioon nähtav siin
<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:msxsl="urn:schemas-microsoft-com:xslt" exclude-result-prefixes="msxsl"
>
<xsl:output method="html" indent="yes"/>
<xsl:template match="/Products">
<html>
<head>
<h3>Tooted</h3>
</head>
<body style="list-style-type:square">
<xsl:for-each select="Product">
<div style="border-style:solid;margin-bottom:5px;width:33%">
<xsl:value-of select="Name/et-EE"/>
<xsl:if test="@parentId">
<span style="color:red"> Toode on hierarhias!</span>
</xsl:if>
<br/><br/>
<xsl:variable name="n" select ="count(Name/*)"/>
<xsl:if test="$n > 1">
Tõlked:
<!-- Tõlked. Kõik mis pole et-EE -->
<xsl:for-each select="Name/*[not(local-name()='et-EE')]">
<li>
<xsl:value-of select="local-name()"/>
<xsl:text>: </xsl:text>
<xsl:value-of select="."/>
</li>
</xsl:for-each>
<br/>
</xsl:if>
Hind <xsl:choose>
<xsl:when test="Prices/@Special">
<strike>
<xsl:value-of select="Prices/@Sales"/>
</strike>
<xsl:text> </xsl:text>
<xsl:value-of select="Prices/@Special"/>
<br/>
<br/>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="Prices/@Sales"/>
<br/>
<br/>
</xsl:otherwise>
</xsl:choose>
<xsl:if test="Properties/*">
Atribuudid:
<xsl:for-each select="Properties/Property">
<li>
<xsl:value-of select="Type"/>
<xsl:text>: </xsl:text>
<xsl:value-of select="Value"/>
</li>
</xsl:for-each>
<br/>
</xsl:if>
<xsl:if test="Availability/*">
Saadavus:
<xsl:for-each select="Availability/Stock">
<li>
<xsl:value-of select="Name"/>
<xsl:text>: </xsl:text>
<xsl:value-of select="AvailableQty"/>
<xsl:text> tk.</xsl:text>
</li>
</xsl:for-each>
<br/>
</xsl:if>
</div>
</xsl:for-each>
</body>
</html>
</xsl:template>
</xsl:stylesheet>
Transformatsioon XMLi
Grupeerime tooted laopõhiselt. Transformatsioon nähtav siin.
<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:msxsl="urn:schemas-microsoft-com:xslt" exclude-result-prefixes="msxsl"
>
<xsl:output method="xml" indent="yes" media-type="text/xml" />
<xsl:key name="groups" match="Product/Availability/Stock/Name" use="."/>
<xsl:template match="Products">
<!-- get unique stocks by Muenchian grouping http://www.jenitennison.com/xslt/grouping/muenchian.html -->
<xsl:for-each select="Product/Availability/Stock/Name[generate-id()=generate-id(key('groups',.)[1])]">
<xsl:variable name="stockID" select="../@id" />
<stock>
<xsl:attribute name="id">
<xsl:value-of select="$stockID"/>
</xsl:attribute>
<name>
<xsl:value-of select="."/>
</name>
<xsl:for-each select="key('groups',.)">
<product>
<xsl:attribute name="id">
<xsl:value-of select="../../../@id"/>
</xsl:attribute>
<xsl:attribute name="qty">
<xsl:value-of select="../../../Availability/Stock[@id=$stockID]/AvailableQty"/>
</xsl:attribute>
<name>
<xsl:value-of select="../../../Name/et-EE" />
</name>
</product>
</xsl:for-each>
</stock>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
Retsensioonid
Veebiteenuse retsensioon meeskonnale GoFood
Projekti arhitektuur vastab headele tavadele ja on üles ehitatud õppejõudude soovituste kohaselt - kasutusel teenuste kiht, mis omakord kasutab uow'd ja repositooriumeid. Kasutusel nii generic kui ka custom repositooriumid. Kood on kirjutatud vastu liideseid (Interface). Kasutusel ka DTO'd mis pannakse kokku Factorites.
Domeenimudel on samuti viisakas. Eksisteerib 12 olemit + kasutajate tabel. Stringid on limiteeritud, PK'd ja FK'd olemas.
Kiiduväärt on ka see, et erinevalt mitmetest teistest proovitud projektidest buildis see esimese katsega ja Migrationid läksid kohe läbi. Eks oleks nice-to-have kui tekitatakse ka kohe mõned andmed millega testida (eriti kuna clientapp kippus vigu andma kui mõned objektid olid tühjad), aga ei olnud suur probleem ka ise andmeid tekitada.
Iga domeeniobjekti kohta eksisteerib kontroller, kuhu on süstitud samanimeline teenus. Siin on ehk tegu väikest viisi liiasusega, sest API seisukohast võiks siduvate tabelite andmeid välja anda läbi root-objekti. Näiteks ProductInShoppingListControlleri võiks ära kaotada ning ShoppingListController võiks neid andmeid välja anda olemasolevate päringute lisana (ShoppingListDTO võiks omada kollektsiooni List<ProductInShoppingList>, mille saaks täita kui EFShoppingListRepositoryst välja anda andmed kujul _db.ShoppingList.Include(x => x.ProductsInShoppingLists). Alternatiivina võiks ProductInShoppingListControlleri meetodid üle viia ShoppingListControllerisse. Sõltumata sellest mis meetodit kasutada, jätaks refereerija alles kontrolleri ainult iga põhilise ’päriselu’ objekti kohta – Store, ShoppingList, Product, NutritionPlan etc ja serveeriks andmeid ainult läbi nende. See teeks ka API tarbija elu lihtsamaks, kuna objektid on intuitiivsemad.
Kõik kontrollerid (v.a SecurityController) on Authorize annotationiga illustreeritud nagu peabki. Seadistatud on ka Swaggeri endpoint.
API funktsionaalsuse poolest on realiseeritud kasutajate loomine, autentimine ja andmebaasi salvestamine ning kontrollerid on valmis iga domeeniobjekti CRUD tegevusteks. Teenuste kihis keerulisemat äriloogikat veel ei ole, toimub suht üks-ühele DTO vs domeeniobjekti mappimine ning seejärel repostiooriumisse või tagasi kontrollerisse saatmine.
Lõpetuseks vaataks üle kuidas projekti suhestub hindamiskriteeriumitega:
- Majanduslik mõtlemine (kas loodav teenus ja rakendused oleks kasutatav ka ärilistel eesmärkidel) – toote sihtgrupp on paigas, idee on üsna huvitav ja ambitsioonikas. Projekti sajaprotsendilise teostuse korral oleks monetiseerimine kindlasti mõeldav
- Mobiilirakendused/Angularis/reactis loodud klientrakendus – klientrakendus teostatud WPF’is. Konkreetne rakendus vajaks ilmselt Native\Hybrid mobiilset äppi, kuna kasutaja peaks tegevusi tegema mobiilsest seadmest erinevatel aegadel päeva jooksul
- Kogukondade kaasamine – sihtgrupp on oma tervisest lugu pidavad inimesed. Ärilise edu korral võib see aidata inimestel teha igapäevaselt paremaid\teadlikumaid valikuid toidu osas ja omab seega positiivset mõju ühiskonnale
- Kasutajamugavus – sõltub eelkõige klientäpi realisatsioonist. API kasutusmugavuse kohta on kommentaarid antud eelnevalt. Swaggeri olemasolu on väga abiks.
- Läbimõeldud töökorraldus – projektil on versioonihaldus (millesse refereerija millegipärast sisse ei saanud), wiki lehel kirjeldatud lühidalt üldine idee ja soovitud funktsionaalsus. Eks analüüs võiks detailsem olla (andmeskeem jne), aga üldjoontes on lähenemine struktuurne
- Lisavõimaluste realiseerimine (vt näidisteemad) – klientäpp on veel üsna algusfaasis, seega on hetkel keskendutud pigem baasfunktsionaalsuse arendamisele
- Korraliku arhitektuuriga kirjutatud kood – projekti ülesehitus korralik, võiks veel julgemalt eksperimenteerida linq’i ja keerulisemate äriloogikate ehitamisel
Klientrakenduse retsensioon meeskonnale GoFood
Klientrakenduse funktsionaalsus:
Käivitamisel tuleb kasutaja registreerida, misjärel on võimalik sisse logida. Ilma registreerimata rakendust kasutada ei saa. Peale sisselogimist on realiseeritud kaks peamist kasutajajuhtu - toitumiskava loomine ning ostunimekirja tegemine. Lisaks sellele ka konto haldus (mis tähendab momendil rakendusest väljalogimist).
Toitumiskava loomisel tuleb sellele anda nimi, seejärel on võimalik tekitada nädalapäeva-spetsiifilisi kavasid. Peale seoste tekitamise momendil rohkem midagi teha ei saa - domeeniolemid on küllaltki algusjärgus ja peale võtmete seal väga palju rohkem salvestada ei ole. Toitumiskava on võimalik ka kustutada. Sel juhul kustutatakse automaatselt ka päevaspetsiifilised kavad.
Ostunimekirja saab luua, peale mida antakse kasutajale võimalus sellesse tooteid lisada. Tooted on jagatud kategooriatesse, peale nimetuse on neil ka kaal, hind ja kasutajaliideses näha erinevad toiteväärtuse indikaatorid. Tootel ja hinnal on üks-mitmene suhe (tootel võib erinevates poodides olla erinev hind), ent päringutes seda momendil ei arvestada ja võetakse esimene leitud hind. Ostunimekirja lisamisel peab kasutaja sisestama koguse, klientrakendus arvutab seejärel vaates lõppsumma kokku. Koguse sisestamisel kontrollitakse, et tegu integeriga, ent sisestada saab ka null või miinusekoguse, mis salvestatakse ka baasi.
Pisut häiris see, et UI ei anna momendil tagasisidet - kas tehtud tegevus õnnestus või ebaõnnestus.
Tehniliselt on API'ga suhtlemiseks loodud baasteenus kus on kirjas API aadress ja loogika tokeni headerisse lisamiseks. Domeeniobjekti-spetsiifilised teenuseklassid pärinevad samast baasklassist. Vaated saavad oma andmed ViewModelitelt, mis omakorda suhtlevad teenustega andmete saamiseks. Refereerija ei oma piisavalt kogemust WPF projektidega, et arhitektuuri süvitsi kommenteerida, ent tundub, et järgitakse MVVM mustrit. Kuna API annab välja puhtalt domeeni-, mitte äriobjekte, siis on ka klientrakendust raske ehitada. Näiteks, et toodetele hinnad arvutada, peab ProductService esiteks tooted pärima (GetProductsFromWebServiceAsync()), seejärel foreach tsüklis iga toote kohta eraldi API hinnapäringu tegema (GetPricesAsync()). Kui ostukorvis on 10 toodet, tehakse API'sse 10 päringut. Selle asemel võiks API välja saata ostukorvi koos toodete ja hindadega - kümne päringu asemel saab siis hakkama ainult ühega.