From d60be0ccaac06d2dcfd9ffb1316d16a6579a63a4 Mon Sep 17 00:00:00 2001 From: Siarhei-Sialitski Date: Fri, 6 Jan 2023 19:22:23 +0100 Subject: [PATCH] Finally deployed --- deploy/k8s/helm/app.yaml | 68 +++--- deploy/k8s/helm/coupon-api/.helmignore | 21 ++ deploy/k8s/helm/coupon-api/Chart.yaml | 5 + .../k8s/helm/coupon-api/templates/NOTES.txt | 9 + .../helm/coupon-api/templates/_helpers.tpl | 32 +++ .../k8s/helm/coupon-api/templates/_names.tpl | 60 ++++++ .../helm/coupon-api/templates/configmap.yaml | 21 ++ .../helm/coupon-api/templates/deployment.yaml | 99 +++++++++ .../helm/coupon-api/templates/service.yaml | 23 ++ deploy/k8s/helm/coupon-api/values.yaml | 61 ++++++ deploy/k8s/helm/deploy-all.ps1 | 88 ++++---- .../webshoppingagg/templates/configmap.yaml | 2 + .../helm/webstatus/templates/configmap.yaml | 2 + deploy/k8s/helm/webstatus/values.yaml | 11 +- .../Mobile.Bff.Shopping/aggregator/Dockerfile | 1 + .../Web.Bff.Shopping/aggregator/Dockerfile | 1 + src/Services/Basket/Basket.API/Dockerfile | 1 + src/Services/Catalog/Catalog.API/Dockerfile | 1 + .../Controllers/CouponController.cs | 30 +++ .../Coupon/Coupon.API/Coupon.API.csproj | 63 ++++++ .../Coupon/Coupon.API/CouponSettings.cs | 15 ++ src/Services/Coupon/Coupon.API/Dockerfile | 62 ++++++ .../Extensions/IHostBuilderExtensions.cs | 49 +++++ .../IServiceCollectionExtensions.cs | 204 ++++++++++++++++++ .../Filters/AuthorizeCheckOperationFilter.cs | 35 +++ .../Filters/ValidateModelAttribute.cs | 16 ++ .../Coupon/Coupon.API/GlobalUsings.cs | 33 +++ .../Coupon.API/Infrastructure/CouponSeed.cs | 52 +++++ .../Infrastructure/Models/Coupon.cs | 20 ++ .../Repositories/CouponContext.cs | 25 +++ .../Repositories/CouponRepository.cs | 42 ++++ .../Repositories/ICouponRepository.cs | 14 ++ ...CouponValidationIntegrationEventHandler.cs | 56 +++++ ...angedToCancelledIntegrationEventHandler.cs | 22 ++ .../OrderCouponConfirmedIntegrationEvent.cs | 14 ++ .../OrderCouponRejectedIntegrationEvent.cs | 14 ++ ...waitingCouponValidationIntegrationEvent.cs | 18 ++ ...tatusChangedToCancelledIntegrationEvent.cs | 18 ++ src/Services/Coupon/Coupon.API/Program.cs | 53 +++++ .../Coupon.API/Properties/launchSettings.json | 12 ++ src/Services/Coupon/Coupon.API/Startup.cs | 94 ++++++++ .../Coupon.API/appsettings.Development.json | 18 ++ .../Coupon/Coupon.API/appsettings.json | 28 +++ src/Services/Identity/Identity.API/Dockerfile | 1 + .../Properties/launchSettings.json | 4 +- src/Services/Ordering/Ordering.API/Dockerfile | 1 + .../Ordering.BackgroundTasks/Dockerfile | 1 + .../Ordering/Ordering.SignalrHub/Dockerfile | 1 + src/Services/Payment/Payment.API/Dockerfile | 1 + src/Services/Webhooks/Webhooks.API/Dockerfile | 1 + src/Web/WebMVC/Dockerfile | 1 + .../src/modules/basket/basket.service.ts | 22 ++ .../orders-detail.component.html | 9 + .../orders-new/orders-new.component.html | 32 +++ .../orders/orders-new/orders-new.component.ts | 29 ++- .../src/modules/orders/orders.service.ts | 9 + .../src/modules/shared/models/coupon.model.ts | 5 + .../shared/models/order-detail.model.ts | 3 + .../src/modules/shared/models/order.model.ts | 3 + src/Web/WebSPA/Dockerfile | 1 + src/Web/WebStatus/Dockerfile | 1 + src/Web/WebhookClient/Dockerfile | 1 + src/docker-compose.yml | 8 + src/eShopOnContainers-ServicesAndWebApps.sln | 58 ++++- 64 files changed, 1619 insertions(+), 86 deletions(-) create mode 100644 deploy/k8s/helm/coupon-api/.helmignore create mode 100644 deploy/k8s/helm/coupon-api/Chart.yaml create mode 100644 deploy/k8s/helm/coupon-api/templates/NOTES.txt create mode 100644 deploy/k8s/helm/coupon-api/templates/_helpers.tpl create mode 100644 deploy/k8s/helm/coupon-api/templates/_names.tpl create mode 100644 deploy/k8s/helm/coupon-api/templates/configmap.yaml create mode 100644 deploy/k8s/helm/coupon-api/templates/deployment.yaml create mode 100644 deploy/k8s/helm/coupon-api/templates/service.yaml create mode 100644 deploy/k8s/helm/coupon-api/values.yaml create mode 100644 src/Services/Coupon/Coupon.API/Controllers/CouponController.cs create mode 100644 src/Services/Coupon/Coupon.API/Coupon.API.csproj create mode 100644 src/Services/Coupon/Coupon.API/CouponSettings.cs create mode 100644 src/Services/Coupon/Coupon.API/Dockerfile create mode 100644 src/Services/Coupon/Coupon.API/Extensions/IHostBuilderExtensions.cs create mode 100644 src/Services/Coupon/Coupon.API/Extensions/IServiceCollectionExtensions.cs create mode 100644 src/Services/Coupon/Coupon.API/Filters/AuthorizeCheckOperationFilter.cs create mode 100644 src/Services/Coupon/Coupon.API/Filters/ValidateModelAttribute.cs create mode 100644 src/Services/Coupon/Coupon.API/GlobalUsings.cs create mode 100644 src/Services/Coupon/Coupon.API/Infrastructure/CouponSeed.cs create mode 100644 src/Services/Coupon/Coupon.API/Infrastructure/Models/Coupon.cs create mode 100644 src/Services/Coupon/Coupon.API/Infrastructure/Repositories/CouponContext.cs create mode 100644 src/Services/Coupon/Coupon.API/Infrastructure/Repositories/CouponRepository.cs create mode 100644 src/Services/Coupon/Coupon.API/Infrastructure/Repositories/ICouponRepository.cs create mode 100644 src/Services/Coupon/Coupon.API/IntegrationEvents/EventHandlers/OrderStatusChangedToAwaitingCouponValidationIntegrationEventHandler.cs create mode 100644 src/Services/Coupon/Coupon.API/IntegrationEvents/EventHandlers/OrderStatusChangedToCancelledIntegrationEventHandler.cs create mode 100644 src/Services/Coupon/Coupon.API/IntegrationEvents/Events/OrderCouponConfirmedIntegrationEvent.cs create mode 100644 src/Services/Coupon/Coupon.API/IntegrationEvents/Events/OrderCouponRejectedIntegrationEvent.cs create mode 100644 src/Services/Coupon/Coupon.API/IntegrationEvents/Events/OrderStatusChangedToAwaitingCouponValidationIntegrationEvent.cs create mode 100644 src/Services/Coupon/Coupon.API/IntegrationEvents/Events/OrderStatusChangedToCancelledIntegrationEvent.cs create mode 100644 src/Services/Coupon/Coupon.API/Program.cs create mode 100644 src/Services/Coupon/Coupon.API/Properties/launchSettings.json create mode 100644 src/Services/Coupon/Coupon.API/Startup.cs create mode 100644 src/Services/Coupon/Coupon.API/appsettings.Development.json create mode 100644 src/Services/Coupon/Coupon.API/appsettings.json create mode 100644 src/Web/WebSPA/Client/src/modules/shared/models/coupon.model.ts diff --git a/deploy/k8s/helm/app.yaml b/deploy/k8s/helm/app.yaml index acea31ef1..e186e8c34 100644 --- a/deploy/k8s/helm/app.yaml +++ b/deploy/k8s/helm/app.yaml @@ -1,38 +1,40 @@ # This helm values file defines app-based settings # Charts use those values, so this file **MUST** be included in all chart releases -app: # app global settings - name: "my-eshop" # Override for custom app name - ingress: # ingress related settings +app: # app global settings + name: "my-eshop" # Override for custom app name + ingress: # ingress related settings entries: - basket: basket-api # ingress entry for basket api - catalog: catalog-api # ingress entry for catalog api - ordering: ordering-api # ingress entry for ordering api - identity: identity # ingress entry for identity api - mvc: webmvc # ingress entry for web mvc - spa: "" # ingress entry for web spa - status: webstatus # ingress entry for web status - webshoppingapigw: webshoppingapigw # ingress entry for web shopping Agw - mobileshoppingapigw: mobileshoppingapigw # ingress entry for mobile shopping Agw - webshoppingagg: webshoppingagg # ingress entry for web shopping aggregator - mobileshoppingagg: mobileshoppingagg # ingress entry for mobile shopping aggregator - payment: payment-api # ingress entry for payment api - webhooks: webhooks-api # ingress entry for webhooks api - webhooksweb: webhooks-web # ingress entry for webhooks web demo client + basket: basket-api # ingress entry for basket api + catalog: catalog-api # ingress entry for catalog api + coupon: coupon-api # ingress entry for coupon api + ordering: ordering-api # ingress entry for ordering api + identity: identity # ingress entry for identity api + mvc: webmvc # ingress entry for web mvc + spa: "" # ingress entry for web spa + status: webstatus # ingress entry for web status + webshoppingapigw: webshoppingapigw # ingress entry for web shopping Agw + mobileshoppingapigw: mobileshoppingapigw # ingress entry for mobile shopping Agw + webshoppingagg: webshoppingagg # ingress entry for web shopping aggregator + mobileshoppingagg: mobileshoppingagg # ingress entry for mobile shopping aggregator + payment: payment-api # ingress entry for payment api + webhooks: webhooks-api # ingress entry for webhooks api + webhooksweb: webhooks-web # ingress entry for webhooks web demo client svc: - basket: basket-api # service name for basket api - catalog: catalog-api # service name for catalog api - ordering: ordering-api # service name for ordering api - orderingbackgroundtasks: ordering-backgroundtasks # service name for orderingbackgroundtasks - orderingsignalrhub: ordering-signalrhub # service name for orderingsignalrhub - identity: identity-api # service name for identity api - mvc: webmvc # service name for web mvc - spa: webspa # service name for web spa - status: webstatus # service name for web status - webshoppingapigw: webshoppingapigw # service name for web shopping Agw - mobileshoppingapigw: mobileshoppingapigw # service name for mobile shopping Agw - webshoppingagg: webshoppingagg # service name for web shopping aggregator - mobileshoppingagg: mobileshoppingagg # service name for mobile shopping aggregator - payment: payment-api # service name for payment api - webhooks: webhooks-api # service name for webhooks api - webhooksweb: webhooks-client # service name for webhooks web + basket: basket-api # service name for basket api + catalog: catalog-api # service name for catalog api + coupon: coupon-api # service name for coupon api + ordering: ordering-api # service name for ordering api + orderingbackgroundtasks: ordering-backgroundtasks # service name for orderingbackgroundtasks + orderingsignalrhub: ordering-signalrhub # service name for orderingsignalrhub + identity: identity-api # service name for identity api + mvc: webmvc # service name for web mvc + spa: webspa # service name for web spa + status: webstatus # service name for web status + webshoppingapigw: webshoppingapigw # service name for web shopping Agw + mobileshoppingapigw: mobileshoppingapigw # service name for mobile shopping Agw + webshoppingagg: webshoppingagg # service name for web shopping aggregator + mobileshoppingagg: mobileshoppingagg # service name for mobile shopping aggregator + payment: payment-api # service name for payment api + webhooks: webhooks-api # service name for webhooks api + webhooksweb: webhooks-client # service name for webhooks web diff --git a/deploy/k8s/helm/coupon-api/.helmignore b/deploy/k8s/helm/coupon-api/.helmignore new file mode 100644 index 000000000..f0c131944 --- /dev/null +++ b/deploy/k8s/helm/coupon-api/.helmignore @@ -0,0 +1,21 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*~ +# Various IDEs +.project +.idea/ +*.tmproj diff --git a/deploy/k8s/helm/coupon-api/Chart.yaml b/deploy/k8s/helm/coupon-api/Chart.yaml new file mode 100644 index 000000000..c110604c3 --- /dev/null +++ b/deploy/k8s/helm/coupon-api/Chart.yaml @@ -0,0 +1,5 @@ +apiVersion: v1 +appVersion: "1.0" +description: A Helm chart for Kubernetes +name: coupon-api +version: 0.1.0 diff --git a/deploy/k8s/helm/coupon-api/templates/NOTES.txt b/deploy/k8s/helm/coupon-api/templates/NOTES.txt new file mode 100644 index 000000000..5c0830985 --- /dev/null +++ b/deploy/k8s/helm/coupon-api/templates/NOTES.txt @@ -0,0 +1,9 @@ +eShop Coupon API installed. +---------------------------- + +This API is not directly exposed outside cluster. If need to access it use: + +export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app={{ template "coupon-api.name" . }},release={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") +echo "Visit http://127.0.0.1:8080 to use your application" +kubectl port-forward $POD_NAME 8080:80 + diff --git a/deploy/k8s/helm/coupon-api/templates/_helpers.tpl b/deploy/k8s/helm/coupon-api/templates/_helpers.tpl new file mode 100644 index 000000000..cd465efaf --- /dev/null +++ b/deploy/k8s/helm/coupon-api/templates/_helpers.tpl @@ -0,0 +1,32 @@ +{{/* vim: set filetype=mustache: */}} +{{/* +Expand the name of the chart. +*/}} +{{- define "coupon-api.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} +{{- end -}} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "coupon-api.fullname" -}} +{{- if .Values.fullnameOverride -}} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}} +{{- else -}} +{{- $name := default .Chart.Name .Values.nameOverride -}} +{{- if contains $name .Release.Name -}} +{{- .Release.Name | trunc 63 | trimSuffix "-" -}} +{{- else -}} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}} +{{- end -}} +{{- end -}} +{{- end -}} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "coupon-api.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}} +{{- end -}} diff --git a/deploy/k8s/helm/coupon-api/templates/_names.tpl b/deploy/k8s/helm/coupon-api/templates/_names.tpl new file mode 100644 index 000000000..605e92e7e --- /dev/null +++ b/deploy/k8s/helm/coupon-api/templates/_names.tpl @@ -0,0 +1,60 @@ +{{- define "suffix-name" -}} +{{- if .Values.app.name -}} +{{- .Values.app.name -}} +{{- else -}} +{{- .Release.Name -}} +{{- end -}} +{{- end -}} + +{{- define "sql-name" -}} +{{- if .Values.inf.sql.host -}} +{{- .Values.inf.sql.host -}} +{{- else -}} +{{- printf "%s" "sql-data" -}} +{{- end -}} +{{- end -}} + +{{- define "mongo-name" -}} +{{- if .Values.inf.mongo.host -}} +{{- .Values.inf.mongo.host -}} +{{- else -}} +{{- printf "%s" "nosql-data" -}} +{{- end -}} +{{- end -}} + +{{- define "url-of" -}} +{{- $name := first .}} +{{- $ctx := last .}} +{{- if eq $name "" -}} +{{- $ctx.Values.inf.k8s.dns -}} +{{- else -}} +{{- printf "%s/%s" $ctx.Values.inf.k8s.dns $name -}} {{/*Value is just / */}} +{{- end -}} +{{- end -}} + + + +{{- define "pathBase" -}} +{{- if .Values.inf.k8s.suffix -}} +{{- $suffix := include "suffix-name" . -}} +{{- printf "%s-%s" .Values.pathBase $suffix -}} +{{- else -}} +{{- .Values.pathBase -}} +{{- end -}} +{{- end -}} + +{{- define "fqdn-image" -}} +{{- if .Values.inf.registry -}} +{{- printf "%s/%s" .Values.inf.registry.server .Values.image.repository -}} +{{- else -}} +{{- .Values.image.repository -}} +{{- end -}} +{{- end -}} + +{{- define "protocol" -}} +{{- if .Values.inf.tls.enabled -}} +{{- printf "%s" "https" -}} +{{- else -}} +{{- printf "%s" "http" -}} +{{- end -}} +{{- end -}} \ No newline at end of file diff --git a/deploy/k8s/helm/coupon-api/templates/configmap.yaml b/deploy/k8s/helm/coupon-api/templates/configmap.yaml new file mode 100644 index 000000000..6372fa246 --- /dev/null +++ b/deploy/k8s/helm/coupon-api/templates/configmap.yaml @@ -0,0 +1,21 @@ +{{- $name := include "coupon-api.fullname" . -}} +{{- $mongo := include "mongo-name" . -}} +{{- $webshoppingapigw := include "url-of" (list .Values.app.ingress.entries.webshoppingapigw .) -}} +{{- $protocol := include "protocol" . -}} + +kind: ConfigMap +apiVersion: v1 +metadata: + name: "cfg-{{ $name }}" + labels: + app: {{ template "coupon-api.name" . }} + chart: {{ template "coupon-api.chart" .}} + release: {{ .Release.Name }} + heritage: {{ .Release.Service }} +data: + coupon__MongoDatabase: "CouponDb" + coupon__ConnectionString: mongodb://nosql-data + urls__IdentityUrl: http://{{ .Values.app.svc.identity }} + all__EventBusConnection: {{ .Values.inf.eventbus.constr }} + all__InstrumentationKey: "{{ .Values.inf.appinsights.key }}" + all__UseAzureServiceBus: "{{ .Values.inf.eventbus.useAzure }}" diff --git a/deploy/k8s/helm/coupon-api/templates/deployment.yaml b/deploy/k8s/helm/coupon-api/templates/deployment.yaml new file mode 100644 index 000000000..95d886df6 --- /dev/null +++ b/deploy/k8s/helm/coupon-api/templates/deployment.yaml @@ -0,0 +1,99 @@ +{{- $name := include "coupon-api.fullname" . -}} +{{- $cfgname := printf "%s-%s" "cfg" $name -}} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ template "coupon-api.fullname" . }} + labels: + ufo: {{ $cfgname}} + app: {{ template "coupon-api.name" . }} + chart: {{ template "coupon-api.chart" . }} + release: {{ .Release.Name }} + heritage: {{ .Release.Service }} +spec: + replicas: {{ .Values.replicaCount }} + selector: + matchLabels: + app: {{ template "coupon-api.name" . }} + release: {{ .Release.Name }} + template: + metadata: + labels: + app: {{ template "coupon-api.name" . }} + release: {{ .Release.Name }} + {{ if .Values.inf.mesh.enabled -}} + annotations: + linkerd.io/inject: enabled + {{- end }} + spec: + {{ if .Values.inf.registry -}} + imagePullSecrets: + - name: {{ .Values.inf.registry.secretName }} + {{- end }} + containers: + - name: {{ .Chart.Name }} + {{ if .Values.probes -}} + {{- if .Values.probes.liveness -}} + livenessProbe: + httpGet: + port: {{ .Values.probes.liveness.port }} + path: {{ .Values.probes.liveness.path }} + initialDelaySeconds: {{ .Values.probes.liveness.initialDelaySeconds }} + periodSeconds: {{ .Values.probes.liveness.periodSeconds }} + {{- end -}} + {{- end -}} + {{- if .Values.probes -}} + {{- if .Values.probes.readiness }} + readinessProbe: + httpGet: + port: {{ .Values.probes.readiness.port }} + path: {{ .Values.probes.readiness.path }} + initialDelaySeconds: {{ .Values.probes.readiness.initialDelaySeconds }} + periodSeconds: {{ .Values.probes.readiness.periodSeconds }} + timeoutSeconds: {{ .Values.probes.readiness.timeoutSeconds }} + {{- end -}} + {{- end }} + image: "{{ template "fqdn-image" . }}:{{ .Values.image.tag }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + env: + - name: PATH_BASE + value: {{ include "pathBase" . }} + - name: k8sname + value: {{ .Values.clusterName }} + {{- if .Values.env.values -}} + {{- range .Values.env.values }} + - name: {{ .name }} + value: {{ .value | quote }} + {{- end -}} + {{- end -}} + {{- if .Values.env.configmap -}} + {{- range .Values.env.configmap }} + - name: {{ .name }} + valueFrom: + configMapKeyRef: + name: {{ $cfgname }} + key: {{ .key }} + {{- end -}} + {{- end }} + ports: + - name: http + containerPort: 80 + protocol: TCP + - name: grpc + containerPort: 81 + protocol: TCP + resources: +{{ toYaml .Values.resources | indent 12 }} + {{- with .Values.nodeSelector }} + nodeSelector: +{{ toYaml . | indent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: +{{ toYaml . | indent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: +{{ toYaml . | indent 8 }} + {{- end }} + diff --git a/deploy/k8s/helm/coupon-api/templates/service.yaml b/deploy/k8s/helm/coupon-api/templates/service.yaml new file mode 100644 index 000000000..7400b5550 --- /dev/null +++ b/deploy/k8s/helm/coupon-api/templates/service.yaml @@ -0,0 +1,23 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ .Values.app.svc.coupon }} + labels: + app: {{ template "coupon-api.name" . }} + chart: {{ template "coupon-api.chart" . }} + release: {{ .Release.Name }} + heritage: {{ .Release.Service }} +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.port }} + targetPort: http + protocol: TCP + name: http + - port: {{ .Values.service.grpcPort }} + targetPort: grpc + protocol: TCP + name: grpc + selector: + app: {{ template "coupon-api.name" . }} + release: {{ .Release.Name }} diff --git a/deploy/k8s/helm/coupon-api/values.yaml b/deploy/k8s/helm/coupon-api/values.yaml new file mode 100644 index 000000000..9d036ecc9 --- /dev/null +++ b/deploy/k8s/helm/coupon-api/values.yaml @@ -0,0 +1,61 @@ +replicaCount: 1 +clusterName: eshop-aks +pathBase: /coupon-api + +image: + repository: eshop/coupon.api + tag: latest + pullPolicy: IfNotPresent + +service: + type: ClusterIP + port: 80 + grpcPort: 81 + +resources: {} + +nodeSelector: {} + +tolerations: [] + +affinity: {} + +# env defines the environment variables that will be declared in the pod +env: + urls: + # configmap declares variables which value is taken from the config map defined in template configmap.yaml (name is name of var and key the key in configmap). + configmap: + - name: ConnectionString + key: coupon__ConnectionString + - name: ApplicationInsights__InstrumentationKey + key: all__InstrumentationKey + - name: EventBusConnection + key: all__EventBusConnection + - name: AzureServiceBusEnabled + key: all__UseAzureServiceBus + - name: CouponMongoDatabase + key: coupon__MongoDatabase + - name: IdentityUrl + key: urls__IdentityUrl + # values define environment variables with a fixed value (no configmap involved) (name is name of var, and value is its value) + values: + - name: ASPNETCORE_ENVIRONMENT + value: Development + - name: OrchestratorType + value: "K8S" + - name: PORT + value: "80" + - name: GRPC_PORT + value: "81" +probes: + liveness: + path: /liveness + initialDelaySeconds: 10 + periodSeconds: 15 + port: 80 + readiness: + path: /hc + timeoutSeconds: 5 + initialDelaySeconds: 90 + periodSeconds: 60 + port: 80 diff --git a/deploy/k8s/helm/deploy-all.ps1 b/deploy/k8s/helm/deploy-all.ps1 index 213e60fb7..24656ddf0 100644 --- a/deploy/k8s/helm/deploy-all.ps1 +++ b/deploy/k8s/helm/deploy-all.ps1 @@ -1,27 +1,27 @@ Param( - [parameter(Mandatory=$false)][string]$registry, - [parameter(Mandatory=$false)][string]$dockerUser, - [parameter(Mandatory=$false)][string]$dockerPassword, - [parameter(Mandatory=$false)][string]$externalDns, - [parameter(Mandatory=$false)][string]$appName="eshop", - [parameter(Mandatory=$false)][bool]$deployInfrastructure=$true, - [parameter(Mandatory=$false)][bool]$deployCharts=$true, - [parameter(Mandatory=$false)][bool]$clean=$true, - [parameter(Mandatory=$false)][string]$aksName="", - [parameter(Mandatory=$false)][string]$aksRg="", - [parameter(Mandatory=$false)][string]$imageTag="latest", - [parameter(Mandatory=$false)][bool]$useLocalk8s=$false, - [parameter(Mandatory=$false)][bool]$useMesh=$false, - [parameter(Mandatory=$false)][string][ValidateSet('Always','IfNotPresent','Never', IgnoreCase=$false)]$imagePullPolicy="Always", - [parameter(Mandatory=$false)][string][ValidateSet('prod','staging','none','custom', IgnoreCase=$false)]$sslSupport = "none", - [parameter(Mandatory=$false)][string]$tlsSecretName = "eshop-tls-custom", - [parameter(Mandatory=$false)][string]$chartsToDeploy="*", - [parameter(Mandatory=$false)][string]$ingressMeshAnnotationsFile="ingress_values_linkerd.yaml" - ) - -function Install-Chart { - Param([string]$chart,[string]$initialOptions, [bool]$customRegistry) - $options=$initialOptions + [parameter(Mandatory = $false)][string]$registry, + [parameter(Mandatory = $false)][string]$dockerUser, + [parameter(Mandatory = $false)][string]$dockerPassword, + [parameter(Mandatory = $false)][string]$externalDns, + [parameter(Mandatory = $false)][string]$appName = "eshop", + [parameter(Mandatory = $false)][bool]$deployInfrastructure = $true, + [parameter(Mandatory = $false)][bool]$deployCharts = $true, + [parameter(Mandatory = $false)][bool]$clean = $true, + [parameter(Mandatory = $false)][string]$aksName = "", + [parameter(Mandatory = $false)][string]$aksRg = "", + [parameter(Mandatory = $false)][string]$imageTag = "latest", + [parameter(Mandatory = $false)][bool]$useLocalk8s = $false, + [parameter(Mandatory = $false)][bool]$useMesh = $false, + [parameter(Mandatory = $false)][string][ValidateSet('Always', 'IfNotPresent', 'Never', IgnoreCase = $false)]$imagePullPolicy = "Always", + [parameter(Mandatory = $false)][string][ValidateSet('prod', 'staging', 'none', 'custom', IgnoreCase = $false)]$sslSupport = "none", + [parameter(Mandatory = $false)][string]$tlsSecretName = "eshop-tls-custom", + [parameter(Mandatory = $false)][string]$chartsToDeploy = "*", + [parameter(Mandatory = $false)][string]$ingressMeshAnnotationsFile = "ingress_values_linkerd.yaml" +) + +function Install-Chart { + Param([string]$chart, [string]$initialOptions, [bool]$customRegistry) + $options = $initialOptions if ($sslEnabled) { $options = "$options --set ingress.tls[0].secretName=$tlsSecretName --set ingress.tls[0].hosts={$dns}" if ($sslSupport -ne "custom") { @@ -32,7 +32,8 @@ function Install-Chart { $options = "$options --set inf.registry.server=$registry --set inf.registry.login=$dockerUser --set inf.registry.pwd=$dockerPassword --set inf.registry.secretName=eshop-docker-scret" } - if ($chart -ne "eshop-common" -or $customRegistry) { # eshop-common is ignored when no secret must be deployed + if ($chart -ne "eshop-common" -or $customRegistry) { + # eshop-common is ignored when no secret must be deployed $command = "install $appName-$chart $options $chart" Write-Host "Helm Command: helm $command" -ForegroundColor Gray Invoke-Expression 'cmd /c "helm $command"' @@ -40,32 +41,32 @@ function Install-Chart { } $dns = $externalDns -$sslEnabled=$false -$sslIssuer="" +$sslEnabled = $false +$sslIssuer = "" if ($sslSupport -eq "staging") { - $sslEnabled=$true - $tlsSecretName="eshop-letsencrypt-staging" - $sslIssuer="letsencrypt-staging" + $sslEnabled = $true + $tlsSecretName = "eshop-letsencrypt-staging" + $sslIssuer = "letsencrypt-staging" } elseif ($sslSupport -eq "prod") { - $sslEnabled=$true - $tlsSecretName="eshop-letsencrypt-prod" - $sslIssuer="letsencrypt-prod" + $sslEnabled = $true + $tlsSecretName = "eshop-letsencrypt-prod" + $sslIssuer = "letsencrypt-prod" } elseif ($sslSupport -eq "custom") { - $sslEnabled=$true + $sslEnabled = $true } -$ingressValuesFile="ingress_values.yaml" +$ingressValuesFile = "ingress_values.yaml" if ($useLocalk8s -eq $true) { - $ingressValuesFile="ingress_values_dockerk8s.yaml" - $dns="localhost" + $ingressValuesFile = "ingress_values_dockerk8s.yaml" + $dns = "localhost" } if ($externalDns -eq "aks") { - if ([string]::IsNullOrEmpty($aksName) -or [string]::IsNullOrEmpty($aksRg)) { + if ([string]::IsNullOrEmpty($aksName) -or [string]::IsNullOrEmpty($aksRg)) { Write-Host "Error: When using -dns aks, MUST set -aksName and -aksRg too." -ForegroundColor Red exit 1 } @@ -95,21 +96,22 @@ if ($useLocalk8s -and $sslEnabled) { } if ($clean) { - $listOfReleases=$(helm ls --filter eshop -q) + $listOfReleases = $(helm ls --filter eshop -q) if ([string]::IsNullOrEmpty($listOfReleases)) { Write-Host "No previous releases found!" -ForegroundColor Green - }else{ + } + else { Write-Host "Previous releases found" -ForegroundColor Green Write-Host "Cleaning previous helm releases..." -ForegroundColor Green helm uninstall $listOfReleases Write-Host "Previous releases deleted" -ForegroundColor Green - } + } } -$useCustomRegistry=$false +$useCustomRegistry = $false if (-not [string]::IsNullOrEmpty($registry)) { - $useCustomRegistry=$true + $useCustomRegistry = $true if ([string]::IsNullOrEmpty($dockerUser) -or [string]::IsNullOrEmpty($dockerPassword)) { Write-Host "Error: Must use -dockerUser AND -dockerPassword if specifying custom registry" -ForegroundColor Red exit 1 @@ -119,7 +121,7 @@ if (-not [string]::IsNullOrEmpty($registry)) { Write-Host "Begin eShopOnContainers installation using Helm" -ForegroundColor Green $infras = ("sql-data", "nosql-data", "rabbitmq", "keystore-data", "basket-data") -$charts = ("eshop-common", "basket-api","catalog-api", "identity-api", "mobileshoppingagg","ordering-api","ordering-backgroundtasks","ordering-signalrhub", "payment-api", "webmvc", "webshoppingagg", "webspa", "webstatus", "webhooks-api", "webhooks-web") +$charts = ("eshop-common", "basket-api", "catalog-api", "coupon-api", "identity-api", "mobileshoppingagg", "ordering-api", "ordering-backgroundtasks", "ordering-signalrhub", "payment-api", "webmvc", "webshoppingagg", "webspa", "webstatus", "webhooks-api", "webhooks-web") $gateways = ("apigwms", "apigwws") if ($deployInfrastructure) { diff --git a/deploy/k8s/helm/webshoppingagg/templates/configmap.yaml b/deploy/k8s/helm/webshoppingagg/templates/configmap.yaml index 0bfb690c1..f10e5bb6d 100644 --- a/deploy/k8s/helm/webshoppingagg/templates/configmap.yaml +++ b/deploy/k8s/helm/webshoppingagg/templates/configmap.yaml @@ -16,10 +16,12 @@ data: webshoppingagg__keystore: {{ .Values.inf.redis.keystore.constr }} internalurls__basket: http://{{ .Values.app.svc.basket }} internalurls__catalog: http://{{ .Values.app.svc.catalog }} + internalurls__coupon: http://{{ .Values.app.svc.coupon }} internalurls__identity: http://{{ .Values.app.svc.identity }} internalurls__ordering: http://{{ .Values.app.svc.ordering }} internalurls__basket__hc: http://{{ .Values.app.svc.basket }}/hc internalurls__catalog__hc: http://{{ .Values.app.svc.catalog }}/hc + internalurls__coupon__hc: http://{{ .Values.app.svc.coupon }}/hc internalurls__identity__hc: http://{{ .Values.app.svc.identity }}/hc internalurls__ordering__hc: http://{{ .Values.app.svc.ordering }}/hc internalurls__payment__hc: http://{{ .Values.app.svc.payment }}/hc diff --git a/deploy/k8s/helm/webstatus/templates/configmap.yaml b/deploy/k8s/helm/webstatus/templates/configmap.yaml index 60bd41489..bee969e09 100644 --- a/deploy/k8s/helm/webstatus/templates/configmap.yaml +++ b/deploy/k8s/helm/webstatus/templates/configmap.yaml @@ -43,3 +43,5 @@ data: internalurls__payment__hc: http://{{ .Values.app.svc.payment }}/hc name__signalrhub__hc: Ordering SignalR Hub HTTP Check internalurls__signalrhub__hc: http://{{ .Values.app.svc.orderingsignalrhub }}/hc + name__coupon__hc: Coupon HTTP Check + internalurls__coupon__hc: http://{{ .Values.app.svc.coupon }}/hc \ No newline at end of file diff --git a/deploy/k8s/helm/webstatus/values.yaml b/deploy/k8s/helm/webstatus/values.yaml index 1ce23fec3..4f7f03a52 100644 --- a/deploy/k8s/helm/webstatus/values.yaml +++ b/deploy/k8s/helm/webstatus/values.yaml @@ -13,14 +13,11 @@ service: ingress: enabled: true - annotations: { - - } + annotations: {} tls: [] resources: {} - nodeSelector: {} tolerations: [] @@ -78,10 +75,14 @@ env: key: name__orderingbackground__hc - name: HealthChecks-UI__HealthChecks__10__Uri key: internalurls__orderingbackground__hc + - name: HealthChecks-UI__HealthChecks__11__Name + key: name__coupon__hc + - name: HealthChecks-UI__HealthChecks__11__Uri + key: internalurls__coupon__hc # values define environment variables with a fixed value (no configmap involved) (name is name of var, and value is its value) values: - name: ASPNETCORE_ENVIRONMENT value: Development - name: OrchestratorType - value: 'K8S' + value: "K8S" diff --git a/src/ApiGateways/Mobile.Bff.Shopping/aggregator/Dockerfile b/src/ApiGateways/Mobile.Bff.Shopping/aggregator/Dockerfile index aab4164b7..4e089c9be 100644 --- a/src/ApiGateways/Mobile.Bff.Shopping/aggregator/Dockerfile +++ b/src/ApiGateways/Mobile.Bff.Shopping/aggregator/Dockerfile @@ -24,6 +24,7 @@ COPY "Services/Basket/Basket.UnitTests/Basket.UnitTests.csproj" "Services/Basket COPY "Services/Catalog/Catalog.API/Catalog.API.csproj" "Services/Catalog/Catalog.API/Catalog.API.csproj" COPY "Services/Catalog/Catalog.FunctionalTests/Catalog.FunctionalTests.csproj" "Services/Catalog/Catalog.FunctionalTests/Catalog.FunctionalTests.csproj" COPY "Services/Catalog/Catalog.UnitTests/Catalog.UnitTests.csproj" "Services/Catalog/Catalog.UnitTests/Catalog.UnitTests.csproj" +COPY "Services/Coupon/Coupon.API/Coupon.API.csproj" "Services/Coupon/Coupon.API/Coupon.API.csproj" COPY "Services/Identity/Identity.API/Identity.API.csproj" "Services/Identity/Identity.API/Identity.API.csproj" COPY "Services/Ordering/Ordering.API/Ordering.API.csproj" "Services/Ordering/Ordering.API/Ordering.API.csproj" COPY "Services/Ordering/Ordering.BackgroundTasks/Ordering.BackgroundTasks.csproj" "Services/Ordering/Ordering.BackgroundTasks/Ordering.BackgroundTasks.csproj" diff --git a/src/ApiGateways/Web.Bff.Shopping/aggregator/Dockerfile b/src/ApiGateways/Web.Bff.Shopping/aggregator/Dockerfile index 8761763a1..016443328 100644 --- a/src/ApiGateways/Web.Bff.Shopping/aggregator/Dockerfile +++ b/src/ApiGateways/Web.Bff.Shopping/aggregator/Dockerfile @@ -24,6 +24,7 @@ COPY "Services/Basket/Basket.UnitTests/Basket.UnitTests.csproj" "Services/Basket COPY "Services/Catalog/Catalog.API/Catalog.API.csproj" "Services/Catalog/Catalog.API/Catalog.API.csproj" COPY "Services/Catalog/Catalog.FunctionalTests/Catalog.FunctionalTests.csproj" "Services/Catalog/Catalog.FunctionalTests/Catalog.FunctionalTests.csproj" COPY "Services/Catalog/Catalog.UnitTests/Catalog.UnitTests.csproj" "Services/Catalog/Catalog.UnitTests/Catalog.UnitTests.csproj" +COPY "Services/Coupon/Coupon.API/Coupon.API.csproj" "Services/Coupon/Coupon.API/Coupon.API.csproj" COPY "Services/Identity/Identity.API/Identity.API.csproj" "Services/Identity/Identity.API/Identity.API.csproj" COPY "Services/Ordering/Ordering.API/Ordering.API.csproj" "Services/Ordering/Ordering.API/Ordering.API.csproj" COPY "Services/Ordering/Ordering.BackgroundTasks/Ordering.BackgroundTasks.csproj" "Services/Ordering/Ordering.BackgroundTasks/Ordering.BackgroundTasks.csproj" diff --git a/src/Services/Basket/Basket.API/Dockerfile b/src/Services/Basket/Basket.API/Dockerfile index 9cd4abba8..d344e804a 100644 --- a/src/Services/Basket/Basket.API/Dockerfile +++ b/src/Services/Basket/Basket.API/Dockerfile @@ -24,6 +24,7 @@ COPY "Services/Basket/Basket.UnitTests/Basket.UnitTests.csproj" "Services/Basket COPY "Services/Catalog/Catalog.API/Catalog.API.csproj" "Services/Catalog/Catalog.API/Catalog.API.csproj" COPY "Services/Catalog/Catalog.FunctionalTests/Catalog.FunctionalTests.csproj" "Services/Catalog/Catalog.FunctionalTests/Catalog.FunctionalTests.csproj" COPY "Services/Catalog/Catalog.UnitTests/Catalog.UnitTests.csproj" "Services/Catalog/Catalog.UnitTests/Catalog.UnitTests.csproj" +COPY "Services/Coupon/Coupon.API/Coupon.API.csproj" "Services/Coupon/Coupon.API/Coupon.API.csproj" COPY "Services/Identity/Identity.API/Identity.API.csproj" "Services/Identity/Identity.API/Identity.API.csproj" COPY "Services/Ordering/Ordering.API/Ordering.API.csproj" "Services/Ordering/Ordering.API/Ordering.API.csproj" COPY "Services/Ordering/Ordering.BackgroundTasks/Ordering.BackgroundTasks.csproj" "Services/Ordering/Ordering.BackgroundTasks/Ordering.BackgroundTasks.csproj" diff --git a/src/Services/Catalog/Catalog.API/Dockerfile b/src/Services/Catalog/Catalog.API/Dockerfile index e491c2110..3c5c963cf 100644 --- a/src/Services/Catalog/Catalog.API/Dockerfile +++ b/src/Services/Catalog/Catalog.API/Dockerfile @@ -25,6 +25,7 @@ COPY "Services/Basket/Basket.UnitTests/Basket.UnitTests.csproj" "Services/Basket COPY "Services/Catalog/Catalog.API/Catalog.API.csproj" "Services/Catalog/Catalog.API/Catalog.API.csproj" COPY "Services/Catalog/Catalog.FunctionalTests/Catalog.FunctionalTests.csproj" "Services/Catalog/Catalog.FunctionalTests/Catalog.FunctionalTests.csproj" COPY "Services/Catalog/Catalog.UnitTests/Catalog.UnitTests.csproj" "Services/Catalog/Catalog.UnitTests/Catalog.UnitTests.csproj" +COPY "Services/Coupon/Coupon.API/Coupon.API.csproj" "Services/Coupon/Coupon.API/Coupon.API.csproj" COPY "Services/Identity/Identity.API/Identity.API.csproj" "Services/Identity/Identity.API/Identity.API.csproj" COPY "Services/Ordering/Ordering.API/Ordering.API.csproj" "Services/Ordering/Ordering.API/Ordering.API.csproj" COPY "Services/Ordering/Ordering.BackgroundTasks/Ordering.BackgroundTasks.csproj" "Services/Ordering/Ordering.BackgroundTasks/Ordering.BackgroundTasks.csproj" diff --git a/src/Services/Coupon/Coupon.API/Controllers/CouponController.cs b/src/Services/Coupon/Coupon.API/Controllers/CouponController.cs new file mode 100644 index 000000000..f2e96ae78 --- /dev/null +++ b/src/Services/Coupon/Coupon.API/Controllers/CouponController.cs @@ -0,0 +1,30 @@ +namespace Coupon.API.Controllers; + +[Authorize] +[ApiController] +[Route("api/v1/[controller]")] +public class CouponController : ControllerBase +{ + private readonly ICouponRepository _couponRepository; + + public CouponController(ICouponRepository couponRepository) + { + _couponRepository = couponRepository; + } + + [HttpGet("{code}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task> GetCouponByCodeAsync(string code) + { + var coupon = await _couponRepository.FindCouponByCodeAsync(code); + + if (coupon is null || coupon.Consumed) + { + return NotFound(); + } + + return coupon; + } +} diff --git a/src/Services/Coupon/Coupon.API/Coupon.API.csproj b/src/Services/Coupon/Coupon.API/Coupon.API.csproj new file mode 100644 index 000000000..e47bc554b --- /dev/null +++ b/src/Services/Coupon/Coupon.API/Coupon.API.csproj @@ -0,0 +1,63 @@ + + + + net6.0 + 1d5bc948-90f1-4906-a1f8-8edaa1ed9e2e + Linux + ..\..\..\.. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Services/Coupon/Coupon.API/CouponSettings.cs b/src/Services/Coupon/Coupon.API/CouponSettings.cs new file mode 100644 index 000000000..f46ac768d --- /dev/null +++ b/src/Services/Coupon/Coupon.API/CouponSettings.cs @@ -0,0 +1,15 @@ +namespace Coupon.API +{ + public class CouponSettings + { + public string ConnectionString { get; set; } + + public string CouponMongoDatabase { get; set; } + + public string EventBusConnection { get; set; } + + public bool UseCustomizationData { get; set; } + + public bool AzureStorageEnabled { get; set; } + } +} diff --git a/src/Services/Coupon/Coupon.API/Dockerfile b/src/Services/Coupon/Coupon.API/Dockerfile new file mode 100644 index 000000000..df44c6918 --- /dev/null +++ b/src/Services/Coupon/Coupon.API/Dockerfile @@ -0,0 +1,62 @@ +#See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for faster debugging. + +FROM mcr.microsoft.com/dotnet/aspnet:6.0 AS base +WORKDIR /app +EXPOSE 80 +EXPOSE 443 + +FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build +WORKDIR /src + +# It's important to keep lines from here down to "COPY . ." identical in all Dockerfiles +# to take advantage of Docker's build cache, to speed up local container builds +COPY "eShopOnContainers-ServicesAndWebApps.sln" "eShopOnContainers-ServicesAndWebApps.sln" + +COPY "ApiGateways/Mobile.Bff.Shopping/aggregator/Mobile.Shopping.HttpAggregator.csproj" "ApiGateways/Mobile.Bff.Shopping/aggregator/Mobile.Shopping.HttpAggregator.csproj" +COPY "ApiGateways/Web.Bff.Shopping/aggregator/Web.Shopping.HttpAggregator.csproj" "ApiGateways/Web.Bff.Shopping/aggregator/Web.Shopping.HttpAggregator.csproj" +COPY "BuildingBlocks/Devspaces.Support/Devspaces.Support.csproj" "BuildingBlocks/Devspaces.Support/Devspaces.Support.csproj" +COPY "BuildingBlocks/EventBus/EventBus/EventBus.csproj" "BuildingBlocks/EventBus/EventBus/EventBus.csproj" +COPY "BuildingBlocks/EventBus/EventBus.Tests/EventBus.Tests.csproj" "BuildingBlocks/EventBus/EventBus.Tests/EventBus.Tests.csproj" +COPY "BuildingBlocks/EventBus/EventBusRabbitMQ/EventBusRabbitMQ.csproj" "BuildingBlocks/EventBus/EventBusRabbitMQ/EventBusRabbitMQ.csproj" +COPY "BuildingBlocks/EventBus/EventBusServiceBus/EventBusServiceBus.csproj" "BuildingBlocks/EventBus/EventBusServiceBus/EventBusServiceBus.csproj" +COPY "BuildingBlocks/EventBus/IntegrationEventLogEF/IntegrationEventLogEF.csproj" "BuildingBlocks/EventBus/IntegrationEventLogEF/IntegrationEventLogEF.csproj" +COPY "BuildingBlocks/WebHostCustomization/WebHost.Customization/WebHost.Customization.csproj" "BuildingBlocks/WebHostCustomization/WebHost.Customization/WebHost.Customization.csproj" +COPY "Services/Basket/Basket.API/Basket.API.csproj" "Services/Basket/Basket.API/Basket.API.csproj" +COPY "Services/Basket/Basket.FunctionalTests/Basket.FunctionalTests.csproj" "Services/Basket/Basket.FunctionalTests/Basket.FunctionalTests.csproj" +COPY "Services/Basket/Basket.UnitTests/Basket.UnitTests.csproj" "Services/Basket/Basket.UnitTests/Basket.UnitTests.csproj" +COPY "Services/Catalog/Catalog.API/Catalog.API.csproj" "Services/Catalog/Catalog.API/Catalog.API.csproj" +COPY "Services/Catalog/Catalog.FunctionalTests/Catalog.FunctionalTests.csproj" "Services/Catalog/Catalog.FunctionalTests/Catalog.FunctionalTests.csproj" +COPY "Services/Catalog/Catalog.UnitTests/Catalog.UnitTests.csproj" "Services/Catalog/Catalog.UnitTests/Catalog.UnitTests.csproj" +COPY "Services/Coupon/Coupon.API/Coupon.API.csproj" "Services/Coupon/Coupon.API/Coupon.API.csproj" +COPY "Services/Identity/Identity.API/Identity.API.csproj" "Services/Identity/Identity.API/Identity.API.csproj" +COPY "Services/Ordering/Ordering.API/Ordering.API.csproj" "Services/Ordering/Ordering.API/Ordering.API.csproj" +COPY "Services/Ordering/Ordering.BackgroundTasks/Ordering.BackgroundTasks.csproj" "Services/Ordering/Ordering.BackgroundTasks/Ordering.BackgroundTasks.csproj" +COPY "Services/Ordering/Ordering.Domain/Ordering.Domain.csproj" "Services/Ordering/Ordering.Domain/Ordering.Domain.csproj" +COPY "Services/Ordering/Ordering.FunctionalTests/Ordering.FunctionalTests.csproj" "Services/Ordering/Ordering.FunctionalTests/Ordering.FunctionalTests.csproj" +COPY "Services/Ordering/Ordering.Infrastructure/Ordering.Infrastructure.csproj" "Services/Ordering/Ordering.Infrastructure/Ordering.Infrastructure.csproj" +COPY "Services/Ordering/Ordering.SignalrHub/Ordering.SignalrHub.csproj" "Services/Ordering/Ordering.SignalrHub/Ordering.SignalrHub.csproj" +COPY "Services/Ordering/Ordering.UnitTests/Ordering.UnitTests.csproj" "Services/Ordering/Ordering.UnitTests/Ordering.UnitTests.csproj" +COPY "Services/Payment/Payment.API/Payment.API.csproj" "Services/Payment/Payment.API/Payment.API.csproj" +COPY "Services/Webhooks/Webhooks.API/Webhooks.API.csproj" "Services/Webhooks/Webhooks.API/Webhooks.API.csproj" +COPY "Tests/Services/Application.FunctionalTests/Application.FunctionalTests.csproj" "Tests/Services/Application.FunctionalTests/Application.FunctionalTests.csproj" +COPY "Web/WebhookClient/WebhookClient.csproj" "Web/WebhookClient/WebhookClient.csproj" +COPY "Web/WebMVC/WebMVC.csproj" "Web/WebMVC/WebMVC.csproj" +COPY "Web/WebSPA/WebSPA.csproj" "Web/WebSPA/WebSPA.csproj" +COPY "Web/WebStatus/WebStatus.csproj" "Web/WebStatus/WebStatus.csproj" + +COPY "docker-compose.dcproj" "docker-compose.dcproj" + +COPY "NuGet.config" "NuGet.config" + +RUN dotnet restore "eShopOnContainers-ServicesAndWebApps.sln" + +COPY . . +WORKDIR "/src/Services/Coupon/Coupon.API" + +FROM build AS publish +RUN dotnet publish "Coupon.API.csproj" -c Release -o /app/publish + +FROM base AS final +WORKDIR /app +COPY --from=publish /app/publish . +ENTRYPOINT ["dotnet", "Coupon.API.dll"] \ No newline at end of file diff --git a/src/Services/Coupon/Coupon.API/Extensions/IHostBuilderExtensions.cs b/src/Services/Coupon/Coupon.API/Extensions/IHostBuilderExtensions.cs new file mode 100644 index 000000000..a5c4e97e9 --- /dev/null +++ b/src/Services/Coupon/Coupon.API/Extensions/IHostBuilderExtensions.cs @@ -0,0 +1,49 @@ +using System; +using Microsoft.Data.SqlClient; +using Coupon.API.IntegrationEvents.EventHandlers; +using Coupon.API.IntegrationEvents.Events; +using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Polly; + +namespace Coupon.API.Extensions +{ + public static class IHostBuilderExtensions + { + public static IHost SeedDatabaseStrategy(this IHost host, Action seeder) + { + using (var scope = host.Services.CreateScope()) + { + var context = scope.ServiceProvider.GetService(); + + var policy = Policy.Handle() + .WaitAndRetry(new TimeSpan[] + { + TimeSpan.FromSeconds(3), + TimeSpan.FromSeconds(5), + TimeSpan.FromSeconds(8), + }); + + policy.Execute(() => + { + seeder.Invoke(context); + }); + } + + return host; + } + + public static IHost SubscribersIntegrationEvents(this IHost host) + { + using (var scope = host.Services.CreateScope()) + { + var eventBus = scope.ServiceProvider.GetRequiredService(); + + eventBus.Subscribe(); + } + + return host; + } + } +} diff --git a/src/Services/Coupon/Coupon.API/Extensions/IServiceCollectionExtensions.cs b/src/Services/Coupon/Coupon.API/Extensions/IServiceCollectionExtensions.cs new file mode 100644 index 000000000..052933c2b --- /dev/null +++ b/src/Services/Coupon/Coupon.API/Extensions/IServiceCollectionExtensions.cs @@ -0,0 +1,204 @@ +namespace Coupon.API.Extensions; + +public static class IServiceCollectionExtensions +{ + public static IServiceCollection AddCouponRegister(this IServiceCollection services, IConfiguration configuration) + { + services.AddTransient() + .AddTransient(service => + { + var settings = service.GetRequiredService>().Value; + var serviceBusConnection = settings.EventBusConnection; + + return new DefaultServiceBusPersisterConnection(serviceBusConnection); + }) + .AddTransient(service => + { + var factory = new ConnectionFactory() + { + HostName = configuration["EventBusConnection"], + DispatchConsumersAsync = true + }; + + var retryCount = 5; + + if (!string.IsNullOrEmpty(configuration["EventBusRetryCount"])) + { + retryCount = int.Parse(configuration["EventBusRetryCount"]); + } + + return new DefaultRabbitMQPersistentConnection(factory, service.GetService>(), retryCount); + }) + .AddTransient() + .AddTransient(); + + return services; + } + + public static IServiceCollection AddSwagger(this IServiceCollection services, IConfiguration configuration) + { + services.AddSwaggerGen(options => + { + options.SwaggerDoc("v1", new OpenApiInfo + { + Title = "eShopOnContainers - Coupon HTTP API", + Version = "v1", + Description = "The Coupon Service HTTP API" + }); + + options.AddSecurityDefinition("oauth2", new OpenApiSecurityScheme + { + Type = SecuritySchemeType.OAuth2, + Flows = new OpenApiOAuthFlows() + { + Implicit = new OpenApiOAuthFlow() + { + AuthorizationUrl = new Uri($"{configuration.GetValue("IdentityUrlExternal")}/connect/authorize"), + TokenUrl = new Uri($"{configuration.GetValue("IdentityUrlExternal")}/connect/token"), + Scopes = new Dictionary() + { + { "coupon", "Coupon API" } + } + } + } + }); + + options.OperationFilter(); + }); + + return services; + } + + public static IServiceCollection AddCustomSettings(this IServiceCollection services, IConfiguration configuration) + { + services.Configure(configuration); + services.Configure(options => + { + options.InvalidModelStateResponseFactory = context => + { + var problemDetails = new ValidationProblemDetails(context.ModelState) + { + Instance = context.HttpContext.Request.Path, + Status = StatusCodes.Status400BadRequest, + Detail = "Please refer to the errors property for additional details." + }; + + return new BadRequestObjectResult(problemDetails) + { + ContentTypes = { "application/problem+json", "application/problem+xml" } + }; + }; + }); + + return services; + } + + public static IServiceCollection AddEventBus(this IServiceCollection services, IConfiguration configuration) + { + var subscriptionClientName = configuration["SubscriptionClientName"]; + + if (configuration.GetValue("AzureServiceBusEnabled")) + { + services.AddSingleton(sp => + { + var serviceBusPersisterConnection = sp.GetRequiredService(); + var iLifetimeScope = sp.GetRequiredService(); + var logger = sp.GetRequiredService>(); + var eventBusSubcriptionsManager = sp.GetRequiredService(); + + return new EventBusServiceBus(serviceBusPersisterConnection, logger, eventBusSubcriptionsManager, iLifetimeScope, subscriptionClientName); + }); + } + else + { + services.AddSingleton(sp => + { + var rabbitMQPersistentConnection = sp.GetRequiredService(); + var iLifetimeScope = sp.GetRequiredService(); + var logger = sp.GetRequiredService>(); + var eventBusSubcriptionsManager = sp.GetRequiredService(); + + var retryCount = 5; + + if (!string.IsNullOrEmpty(configuration["EventBusRetryCount"])) + { + retryCount = int.Parse(configuration["EventBusRetryCount"]); + } + + return new EventBusRabbitMQ(rabbitMQPersistentConnection, logger, iLifetimeScope, eventBusSubcriptionsManager, subscriptionClientName, retryCount); + }); + } + + return services; + } + + public static IServiceCollection AddCustomHealthCheck(this IServiceCollection services, IConfiguration configuration) + { + var accountName = configuration.GetValue("AzureStorageAccountName"); + var accountKey = configuration.GetValue("AzureStorageAccountKey"); + + var hcBuilder = services.AddHealthChecks(); + + hcBuilder.AddCheck("self", () => HealthCheckResult.Healthy()) + .AddMongoDb( + configuration["ConnectionString"], + name: "CouponCollection-check", + tags: new string[] { "couponcollection" }); + + if (configuration.GetValue("AzureServiceBusEnabled")) + { + hcBuilder.AddAzureServiceBusTopic( + configuration["EventBusConnection"], + topicName: "eshop_event_bus", + name: "coupon-servicebus-check", + tags: new string[] { "servicebus" }); + } + else + { + hcBuilder.AddRabbitMQ( + $"amqp://{configuration["EventBusConnection"]}", + name: "coupon-rabbitmqbus-check", + tags: new string[] { "rabbitmqbus" }); + } + + return services; + } + + public static IServiceCollection AddCustomPolicies(this IServiceCollection services) + { + services.AddCors(options => + { + options.AddPolicy("CorsPolicy", + builder => builder.SetIsOriginAllowed((host) => true) + .AllowAnyMethod() + .AllowAnyHeader() + .AllowCredentials()); + }); + + return services; + } + + public static IServiceCollection AddAppInsights(this IServiceCollection services, IConfiguration configuration) + { + services.AddApplicationInsightsTelemetry(configuration); + services.AddApplicationInsightsKubernetesEnricher(); + + return services; + } + + public static IServiceCollection AddCustomAuthentication(this IServiceCollection services, IConfiguration configuration) + { + return services.AddAuthentication(options => + { + options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; + options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; + }).AddJwtBearer(options => + { + options.Authority = configuration["IdentityUrl"]; + options.RequireHttpsMetadata = false; + options.Audience = "coupon"; + }).Services; + } + + public static IServiceCollection AddCustomAuthorization(this IServiceCollection services) => services.AddAuthorization(); +} diff --git a/src/Services/Coupon/Coupon.API/Filters/AuthorizeCheckOperationFilter.cs b/src/Services/Coupon/Coupon.API/Filters/AuthorizeCheckOperationFilter.cs new file mode 100644 index 000000000..726b537d4 --- /dev/null +++ b/src/Services/Coupon/Coupon.API/Filters/AuthorizeCheckOperationFilter.cs @@ -0,0 +1,35 @@ +using System.Collections.Generic; +using System.Linq; +using Microsoft.AspNetCore.Authorization; +using Microsoft.OpenApi.Models; +using Swashbuckle.AspNetCore.SwaggerGen; + +namespace Coupon.API.Filters +{ + public class AuthorizeCheckOperationFilter : IOperationFilter + { + public void Apply(OpenApiOperation operation, OperationFilterContext context) + { + var hasAuthorize = context.MethodInfo.DeclaringType.GetCustomAttributes(true).OfType().Any() || + context.MethodInfo.GetCustomAttributes(true).OfType().Any(); + + if (!hasAuthorize) return; + + operation.Responses.TryAdd("401", new OpenApiResponse { Description = "Unauthorized" }); + operation.Responses.TryAdd("403", new OpenApiResponse { Description = "Forbidden" }); + + var oAuthScheme = new OpenApiSecurityScheme + { + Reference = new OpenApiReference { Type = ReferenceType.SecurityScheme, Id = "oauth2" } + }; + + operation.Security = new List + { + new OpenApiSecurityRequirement + { + [oAuthScheme] = new [] { "CouponApi" } + } + }; + } + } +} diff --git a/src/Services/Coupon/Coupon.API/Filters/ValidateModelAttribute.cs b/src/Services/Coupon/Coupon.API/Filters/ValidateModelAttribute.cs new file mode 100644 index 000000000..59cc10917 --- /dev/null +++ b/src/Services/Coupon/Coupon.API/Filters/ValidateModelAttribute.cs @@ -0,0 +1,16 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; + +namespace Coupon.API.Filters +{ + public class ValidateModelAttribute : ActionFilterAttribute + { + public override void OnActionExecuting(ActionExecutingContext context) + { + if (!context.ModelState.IsValid) + { + context.Result = new BadRequestObjectResult(context.ModelState); + } + } + } +} diff --git a/src/Services/Coupon/Coupon.API/GlobalUsings.cs b/src/Services/Coupon/Coupon.API/GlobalUsings.cs new file mode 100644 index 000000000..290d0725e --- /dev/null +++ b/src/Services/Coupon/Coupon.API/GlobalUsings.cs @@ -0,0 +1,33 @@ +global using Coupon.API.Filters; +global using Coupon.API.Infrastructure.Repositories; +global using Coupon.API.Infrastructure.Models; +global using Autofac.Extensions.DependencyInjection; +global using Autofac; +global using Microsoft.AspNetCore.Authorization; +global using Microsoft.AspNetCore.Hosting; +global using Microsoft.AspNetCore.Http; +global using Microsoft.AspNetCore.Builder; +global using Microsoft.AspNetCore.Mvc.Filters; +global using Microsoft.AspNetCore.Mvc; +global using Microsoft.Extensions.Logging; +global using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions; +global using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events; +global using Microsoft.Extensions.Configuration; +global using Microsoft.Extensions.DependencyInjection; +global using Microsoft.Extensions.Hosting; +global using Microsoft.Extensions.Options; +global using Polly; +global using Serilog.Context; +global using Serilog; +global using System.Collections.Generic; +global using System.IO; +global using System.Linq; +global using System.Threading.Tasks; +global using System; +global using Microsoft.eShopOnContainers.BuildingBlocks.EventBus; +global using Microsoft.eShopOnContainers.BuildingBlocks.EventBusRabbitMQ; +global using Microsoft.eShopOnContainers.BuildingBlocks.EventBusServiceBus; +global using Microsoft.Extensions.Diagnostics.HealthChecks; +global using Microsoft.OpenApi.Models; +global using RabbitMQ.Client; +global using Microsoft.AspNetCore.Authentication.JwtBearer; diff --git a/src/Services/Coupon/Coupon.API/Infrastructure/CouponSeed.cs b/src/Services/Coupon/Coupon.API/Infrastructure/CouponSeed.cs new file mode 100644 index 000000000..39c531381 --- /dev/null +++ b/src/Services/Coupon/Coupon.API/Infrastructure/CouponSeed.cs @@ -0,0 +1,52 @@ +namespace Coupon.API.Infrastructure +{ + using System.Collections.Generic; + using System.Threading.Tasks; + using Coupon.API.Infrastructure.Models; + using Coupon.API.Infrastructure.Repositories; + + public class CouponSeed + { + public async Task SeedAsync(CouponContext context) + { + if (context.Coupons.EstimatedDocumentCount() == 0) + { + var coupons = new List + { + new Coupon + { + Code = "DISC-5", + Discount = 5 + }, + new Coupon + { + Code = "DISC-10", + Discount = 10 + }, + new Coupon + { + Code = "DISC-15", + Discount = 15 + }, + new Coupon + { + Code = "DISC-20", + Discount = 20 + }, + new Coupon + { + Code = "DISC-25", + Discount = 25 + }, + new Coupon + { + Code = "DISC-30", + Discount = 30 + } + }; + + await context.Coupons.InsertManyAsync(coupons); + } + } + } +} diff --git a/src/Services/Coupon/Coupon.API/Infrastructure/Models/Coupon.cs b/src/Services/Coupon/Coupon.API/Infrastructure/Models/Coupon.cs new file mode 100644 index 000000000..13d628dbc --- /dev/null +++ b/src/Services/Coupon/Coupon.API/Infrastructure/Models/Coupon.cs @@ -0,0 +1,20 @@ +using MongoDB.Bson; +using MongoDB.Bson.Serialization.Attributes; + +namespace Coupon.API.Infrastructure.Models +{ + public class Coupon + { + [BsonIgnoreIfDefault] + [BsonRepresentation(BsonType.ObjectId)] + public string Id { get; set; } + + public int Discount { get; set; } + + public string Code { get; set; } + + public bool Consumed { get; set; } + + public int OrderId { get; set; } + } +} diff --git a/src/Services/Coupon/Coupon.API/Infrastructure/Repositories/CouponContext.cs b/src/Services/Coupon/Coupon.API/Infrastructure/Repositories/CouponContext.cs new file mode 100644 index 000000000..945dfe4f0 --- /dev/null +++ b/src/Services/Coupon/Coupon.API/Infrastructure/Repositories/CouponContext.cs @@ -0,0 +1,25 @@ +namespace Coupon.API.Infrastructure.Repositories +{ + using Coupon.API.Infrastructure.Models; + using Microsoft.Extensions.Options; + using MongoDB.Driver; + + public class CouponContext + { + private readonly IMongoDatabase _database = null; + + public CouponContext(IOptions settings) + { + var client = new MongoClient(settings.Value.ConnectionString); + + if (client is null) + { + throw new MongoConfigurationException("Cannot connect to the database. The connection string is not valid or the database is not accessible"); + } + + _database = client.GetDatabase(settings.Value.CouponMongoDatabase); + } + + public IMongoCollection Coupons => _database.GetCollection("CouponCollection"); + } +} diff --git a/src/Services/Coupon/Coupon.API/Infrastructure/Repositories/CouponRepository.cs b/src/Services/Coupon/Coupon.API/Infrastructure/Repositories/CouponRepository.cs new file mode 100644 index 000000000..62bd8a3fa --- /dev/null +++ b/src/Services/Coupon/Coupon.API/Infrastructure/Repositories/CouponRepository.cs @@ -0,0 +1,42 @@ +namespace Coupon.API.Infrastructure.Repositories +{ + using System.Threading.Tasks; + using Coupon.API.Infrastructure.Models; + using MongoDB.Driver; + + public class CouponRepository : ICouponRepository + { + private readonly CouponContext _couponContext; + + public CouponRepository(CouponContext couponContext) + { + _couponContext = couponContext; + } + + public async Task UpdateCouponConsumedByCodeAsync(string code, int orderId) + { + var filter = Builders.Filter.Eq("Code", code); + var update = Builders.Update + .Set(coupon => coupon.Consumed, true) + .Set(coupon => coupon.OrderId, orderId); + + await _couponContext.Coupons.UpdateOneAsync(filter, update, new UpdateOptions { IsUpsert = false }); + } + + public async Task UpdateCouponReleasedByOrderIdAsync(int orderId) + { + var filter = Builders.Filter.Eq("OrderId", orderId); + var update = Builders.Update + .Set(coupon => coupon.Consumed, false) + .Set(coupon => coupon.OrderId, 0); + + await _couponContext.Coupons.UpdateOneAsync(filter, update, new UpdateOptions { IsUpsert = false }); + } + + public async Task FindCouponByCodeAsync(string code) + { + var filter = Builders.Filter.Eq("Code", code); + return await _couponContext.Coupons.Find(filter).FirstOrDefaultAsync(); + } + } +} diff --git a/src/Services/Coupon/Coupon.API/Infrastructure/Repositories/ICouponRepository.cs b/src/Services/Coupon/Coupon.API/Infrastructure/Repositories/ICouponRepository.cs new file mode 100644 index 000000000..0dbac0693 --- /dev/null +++ b/src/Services/Coupon/Coupon.API/Infrastructure/Repositories/ICouponRepository.cs @@ -0,0 +1,14 @@ +namespace Coupon.API.Infrastructure.Repositories +{ + using System.Threading.Tasks; + using Coupon.API.Infrastructure.Models; + + public interface ICouponRepository + { + Task FindCouponByCodeAsync(string code); + + Task UpdateCouponConsumedByCodeAsync(string code, int orderId); + + Task UpdateCouponReleasedByOrderIdAsync(int orderId); + } +} diff --git a/src/Services/Coupon/Coupon.API/IntegrationEvents/EventHandlers/OrderStatusChangedToAwaitingCouponValidationIntegrationEventHandler.cs b/src/Services/Coupon/Coupon.API/IntegrationEvents/EventHandlers/OrderStatusChangedToAwaitingCouponValidationIntegrationEventHandler.cs new file mode 100644 index 000000000..d7f76acab --- /dev/null +++ b/src/Services/Coupon/Coupon.API/IntegrationEvents/EventHandlers/OrderStatusChangedToAwaitingCouponValidationIntegrationEventHandler.cs @@ -0,0 +1,56 @@ +using System.Threading.Tasks; +using Coupon.API.Infrastructure.Repositories; +using Coupon.API.IntegrationEvents.Events; +using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions; +using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events; +using Serilog; +using Serilog.Context; + +namespace Coupon.API.IntegrationEvents.EventHandlers +{ + public class OrderStatusChangedToAwaitingCouponValidationIntegrationEventHandler : IIntegrationEventHandler + { + private readonly ICouponRepository _couponRepository; + private readonly IEventBus _eventBus; + + public OrderStatusChangedToAwaitingCouponValidationIntegrationEventHandler(ICouponRepository couponRepository, IEventBus eventBus) + { + _couponRepository = couponRepository; + _eventBus = eventBus; + } + + public async Task Handle(OrderStatusChangedToAwaitingCouponValidationIntegrationEvent @event) + { + await Task.Delay(3000); + + using (LogContext.PushProperty("IntegrationEventContext", $"{@event.Id}-Coupon.API")) + { + Log.Information("----- Handling integration event: {IntegrationEventId} at {AppName} - ({@IntegrationEvent})", @event.Id, "Coupon.API", @event); + + var couponIntegrationEvent = await ProcessIntegrationEventAsync(@event); + + Log.Information("----- Publishing integration event: {IntegrationEventId} from {AppName} - ({@IntegrationEvent})", couponIntegrationEvent.Id, "Coupon.API", couponIntegrationEvent); + + _eventBus.Publish(couponIntegrationEvent); + } + } + + private async Task ProcessIntegrationEventAsync(OrderStatusChangedToAwaitingCouponValidationIntegrationEvent integrationEvent) + { + var coupon = await _couponRepository.FindCouponByCodeAsync(integrationEvent.Code); + + Log.Information("----- Coupon \"{CouponCode}\": {@Coupon}", integrationEvent.Code, coupon); + + if (coupon == null || coupon.Consumed) + { + return new OrderCouponRejectedIntegrationEvent(integrationEvent.OrderId, coupon.Code); + } + + Log.Information("Consumed coupon: {DiscountCode}", integrationEvent.Code); + + await _couponRepository.UpdateCouponConsumedByCodeAsync(integrationEvent.Code, integrationEvent.OrderId); + + return new OrderCouponConfirmedIntegrationEvent(integrationEvent.OrderId, coupon.Discount); + } + } +} diff --git a/src/Services/Coupon/Coupon.API/IntegrationEvents/EventHandlers/OrderStatusChangedToCancelledIntegrationEventHandler.cs b/src/Services/Coupon/Coupon.API/IntegrationEvents/EventHandlers/OrderStatusChangedToCancelledIntegrationEventHandler.cs new file mode 100644 index 000000000..c6b931e40 --- /dev/null +++ b/src/Services/Coupon/Coupon.API/IntegrationEvents/EventHandlers/OrderStatusChangedToCancelledIntegrationEventHandler.cs @@ -0,0 +1,22 @@ +using System.Threading.Tasks; +using Coupon.API.Infrastructure.Repositories; +using Coupon.API.IntegrationEvents.Events; +using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions; + +namespace Coupon.API.IntegrationEvents.EventHandlers +{ + public class OrderStatusChangedToCancelledIntegrationEventHandler : IIntegrationEventHandler + { + private readonly ICouponRepository _couponRepository; + + public OrderStatusChangedToCancelledIntegrationEventHandler(ICouponRepository couponRepository) + { + _couponRepository = couponRepository; + } + + public async Task Handle(OrderStatusChangedToCancelledIntegrationEvent @event) + { + await _couponRepository.UpdateCouponReleasedByOrderIdAsync(@event.OrderId); + } + } +} diff --git a/src/Services/Coupon/Coupon.API/IntegrationEvents/Events/OrderCouponConfirmedIntegrationEvent.cs b/src/Services/Coupon/Coupon.API/IntegrationEvents/Events/OrderCouponConfirmedIntegrationEvent.cs new file mode 100644 index 000000000..ab749e4d1 --- /dev/null +++ b/src/Services/Coupon/Coupon.API/IntegrationEvents/Events/OrderCouponConfirmedIntegrationEvent.cs @@ -0,0 +1,14 @@ +namespace Coupon.API.IntegrationEvents.Events; + +public record OrderCouponConfirmedIntegrationEvent : IntegrationEvent +{ + public int OrderId { get; } + + public int Discount { get; } + + public OrderCouponConfirmedIntegrationEvent(int orderId, int discount) + { + OrderId = orderId; + Discount = discount; + } +} diff --git a/src/Services/Coupon/Coupon.API/IntegrationEvents/Events/OrderCouponRejectedIntegrationEvent.cs b/src/Services/Coupon/Coupon.API/IntegrationEvents/Events/OrderCouponRejectedIntegrationEvent.cs new file mode 100644 index 000000000..87d218783 --- /dev/null +++ b/src/Services/Coupon/Coupon.API/IntegrationEvents/Events/OrderCouponRejectedIntegrationEvent.cs @@ -0,0 +1,14 @@ +namespace Coupon.API.IntegrationEvents.Events; + +public record OrderCouponRejectedIntegrationEvent : IntegrationEvent +{ + public int OrderId { get; } + + public string Code { get; } + + public OrderCouponRejectedIntegrationEvent(int orderId, string code) + { + OrderId = orderId; + Code = code; + } +} diff --git a/src/Services/Coupon/Coupon.API/IntegrationEvents/Events/OrderStatusChangedToAwaitingCouponValidationIntegrationEvent.cs b/src/Services/Coupon/Coupon.API/IntegrationEvents/Events/OrderStatusChangedToAwaitingCouponValidationIntegrationEvent.cs new file mode 100644 index 000000000..890bfe29e --- /dev/null +++ b/src/Services/Coupon/Coupon.API/IntegrationEvents/Events/OrderStatusChangedToAwaitingCouponValidationIntegrationEvent.cs @@ -0,0 +1,18 @@ +using Newtonsoft.Json; + +namespace Coupon.API.IntegrationEvents.Events; + +public record OrderStatusChangedToAwaitingCouponValidationIntegrationEvent : IntegrationEvent +{ + [JsonProperty] + public int OrderId { get; private set; } + + [JsonProperty] + public string OrderStatus { get; private set; } + + [JsonProperty] + public string BuyerName { get; private set; } + + [JsonProperty] + public string Code { get; private set; } +} diff --git a/src/Services/Coupon/Coupon.API/IntegrationEvents/Events/OrderStatusChangedToCancelledIntegrationEvent.cs b/src/Services/Coupon/Coupon.API/IntegrationEvents/Events/OrderStatusChangedToCancelledIntegrationEvent.cs new file mode 100644 index 000000000..6ec1cec8a --- /dev/null +++ b/src/Services/Coupon/Coupon.API/IntegrationEvents/Events/OrderStatusChangedToCancelledIntegrationEvent.cs @@ -0,0 +1,18 @@ +using Newtonsoft.Json; + +namespace Coupon.API.IntegrationEvents.Events; + +public record OrderStatusChangedToCancelledIntegrationEvent : IntegrationEvent +{ + [JsonProperty] + public int OrderId { get; private set; } + + [JsonProperty] + public string OrderStatus { get; private set; } + + [JsonProperty] + public string BuyerName { get; private set; } + + [JsonProperty] + public string DiscountCode { get; private set; } +} diff --git a/src/Services/Coupon/Coupon.API/Program.cs b/src/Services/Coupon/Coupon.API/Program.cs new file mode 100644 index 000000000..e00001af0 --- /dev/null +++ b/src/Services/Coupon/Coupon.API/Program.cs @@ -0,0 +1,53 @@ +using System.IO; +using Autofac.Extensions.DependencyInjection; +using Coupon.API.Extensions; +using Coupon.API.Infrastructure; +using Coupon.API.Infrastructure.Repositories; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; +using Serilog; +using Serilog.Events; + +namespace Coupon.API +{ + public class Program + { + public static void Main(string[] args) => + CreateHostBuilder(args) + .Build() + .SeedDatabaseStrategy(context => new CouponSeed().SeedAsync(context).Wait()) + .SubscribersIntegrationEvents() + .Run(); + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .UseServiceProviderFactory(new AutofacServiceProviderFactory()) + .ConfigureAppConfiguration((host, builder) => + { + builder.SetBasePath(Directory.GetCurrentDirectory()) + .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true) + .AddJsonFile($"appsettings.{host.HostingEnvironment.EnvironmentName}.json", optional: false, reloadOnChange: true) + .AddEnvironmentVariables(); + + var config = builder.Build(); + + if (config.GetValue("UseVault", false)) + { + builder.AddAzureKeyVault($"https://{config["Vault:Name"]}.vault.azure.net/", config["Vault:ClientId"], config["Vault:ClientSecret"]); + } + }) + .ConfigureWebHostDefaults(webBuilder => webBuilder.UseStartup()) + .UseSerilog((host, builder) => + { + builder.MinimumLevel.Verbose() + .MinimumLevel.Override("Microsoft", LogEventLevel.Information) + .Enrich.WithProperty("ApplicationContext", host.HostingEnvironment.ApplicationName) + .Enrich.FromLogContext() + .WriteTo.Console() + .WriteTo.Seq(string.IsNullOrWhiteSpace(host.Configuration["Serilog:SeqServerUrl"]) ? "http://seq" : host.Configuration["Serilog:SeqServerUrl"]) + .WriteTo.Http(string.IsNullOrWhiteSpace(host.Configuration["Serilog:LogstashUrl"]) ? "http://logstash:8080" : host.Configuration["Serilog:LogstashUrl"]) + .ReadFrom.Configuration(host.Configuration); + }); + } +} diff --git a/src/Services/Coupon/Coupon.API/Properties/launchSettings.json b/src/Services/Coupon/Coupon.API/Properties/launchSettings.json new file mode 100644 index 000000000..a8cfdad45 --- /dev/null +++ b/src/Services/Coupon/Coupon.API/Properties/launchSettings.json @@ -0,0 +1,12 @@ +{ + "profiles": { + "Coupon.API": { + "commandName": "Project", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "https://localhost:49935;http://localhost:49936" + } + } +} \ No newline at end of file diff --git a/src/Services/Coupon/Coupon.API/Startup.cs b/src/Services/Coupon/Coupon.API/Startup.cs new file mode 100644 index 000000000..1600e1426 --- /dev/null +++ b/src/Services/Coupon/Coupon.API/Startup.cs @@ -0,0 +1,94 @@ +using Coupon.API.Extensions; +using Coupon.API.Filters; +using Coupon.API.IntegrationEvents.EventHandlers; +using Coupon.API.IntegrationEvents.Events; +using HealthChecks.UI.Client; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Diagnostics.HealthChecks; +using Microsoft.AspNetCore.Hosting; +using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Serilog; + +namespace Coupon.API +{ + public class Startup + { + public Startup(IConfiguration configuration) + { + Configuration = configuration; + } + + public IConfiguration Configuration { get; } + + public void ConfigureServices(IServiceCollection services) + { + services.AddControllers(options => options.Filters.Add()); + + services.AddCustomSettings(Configuration) + .AddCouponRegister(Configuration) + .AddCustomPolicies() + .AddAppInsights(Configuration) + .AddEventBus(Configuration) + .AddCustomAuthentication(Configuration) + .AddCustomAuthorization() + .AddSwagger(Configuration) + .AddCustomHealthCheck(Configuration); + + services.AddTransient, OrderStatusChangedToAwaitingCouponValidationIntegrationEventHandler>(); + services.AddTransient, OrderStatusChangedToCancelledIntegrationEventHandler>(); + } + + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + + var pathBase = Configuration["PATH_BASE"]; + + if (!string.IsNullOrEmpty(pathBase)) + { + app.UsePathBase(pathBase); + } + + app.UseSwagger() + .UseSwaggerUI(options => + { + options.SwaggerEndpoint($"{ (!string.IsNullOrEmpty(pathBase) ? pathBase : string.Empty) }/swagger/v1/swagger.json", "Coupon.API V1"); + options.OAuthClientId("couponswaggerui"); + options.OAuthAppName("eShop-Learn.Coupon.API Swagger UI"); + }) + .UseCors("CorsPolicy") + .UseRouting() + .UseAuthentication() + .UseAuthorization() + .UseEndpoints(endpoints => + { + endpoints.MapControllers(); + endpoints.MapHealthChecks("/hc", new HealthCheckOptions + { + Predicate = _ => true, + ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse + }); + endpoints.MapHealthChecks("/liveness", new HealthCheckOptions + { + Predicate = r => r.Name.Contains("self") + }); + }); + + ConfigureEventBus(app); + } + + private void ConfigureEventBus(IApplicationBuilder app) + { + var eventBus = app.ApplicationServices.GetRequiredService(); + + eventBus.Subscribe>(); + eventBus.Subscribe>(); + } + } +} diff --git a/src/Services/Coupon/Coupon.API/appsettings.Development.json b/src/Services/Coupon/Coupon.API/appsettings.Development.json new file mode 100644 index 000000000..a00a59a10 --- /dev/null +++ b/src/Services/Coupon/Coupon.API/appsettings.Development.json @@ -0,0 +1,18 @@ +{ + "ConnectionString": "mongodb://localhost:27017", + "CouponMongoDatabase": "CouponDb", + "Serilog": { + "MinimumLevel": { + "Default": "Debug", + "Override": { + "Microsoft": "Warning", + "Microsoft.eShopOnContainers": "Debug", + "System": "Warning" + } + } + }, + "IdentityUrlExternal": "http://localhost:5105", + "IdentityUrl": "http://localhost:5105", + "AzureServiceBusEnabled": false, + "EventBusConnection": "localhost" +} diff --git a/src/Services/Coupon/Coupon.API/appsettings.json b/src/Services/Coupon/Coupon.API/appsettings.json new file mode 100644 index 000000000..d93e3cd7f --- /dev/null +++ b/src/Services/Coupon/Coupon.API/appsettings.json @@ -0,0 +1,28 @@ +{ + "ConnectionString": null, + "CouponMongoDatabase": "CouponDb", + "UseCustomizationData": false, + "Serilog": { + "SeqServerUrl": null, + "LogstashUrl": null, + "MinimumLevel": { + "Default": "Information", + "Override": { + "Microsoft": "Warning", + "Microsoft.eShopOnContainers": "Information", + "System": "Warning" + } + } + }, + "SubscriptionClientName": "Coupon", + "ApplicationInsights": { + "InstrumentationKey": "" + }, + "EventBusRetryCount": 5, + "UseVault": false, + "Vault": { + "Name": "eshop", + "ClientId": "your-client-id", + "ClientSecret": "your-client-secret" + } +} diff --git a/src/Services/Identity/Identity.API/Dockerfile b/src/Services/Identity/Identity.API/Dockerfile index 674cc4ec1..62c3bc028 100644 --- a/src/Services/Identity/Identity.API/Dockerfile +++ b/src/Services/Identity/Identity.API/Dockerfile @@ -24,6 +24,7 @@ COPY "Services/Basket/Basket.UnitTests/Basket.UnitTests.csproj" "Services/Basket COPY "Services/Catalog/Catalog.API/Catalog.API.csproj" "Services/Catalog/Catalog.API/Catalog.API.csproj" COPY "Services/Catalog/Catalog.FunctionalTests/Catalog.FunctionalTests.csproj" "Services/Catalog/Catalog.FunctionalTests/Catalog.FunctionalTests.csproj" COPY "Services/Catalog/Catalog.UnitTests/Catalog.UnitTests.csproj" "Services/Catalog/Catalog.UnitTests/Catalog.UnitTests.csproj" +COPY "Services/Coupon/Coupon.API/Coupon.API.csproj" "Services/Coupon/Coupon.API/Coupon.API.csproj" COPY "Services/Identity/Identity.API/Identity.API.csproj" "Services/Identity/Identity.API/Identity.API.csproj" COPY "Services/Ordering/Ordering.API/Ordering.API.csproj" "Services/Ordering/Ordering.API/Ordering.API.csproj" COPY "Services/Ordering/Ordering.BackgroundTasks/Ordering.BackgroundTasks.csproj" "Services/Ordering/Ordering.BackgroundTasks/Ordering.BackgroundTasks.csproj" diff --git a/src/Services/Identity/Identity.API/Properties/launchSettings.json b/src/Services/Identity/Identity.API/Properties/launchSettings.json index e52e9f99c..2f1a64725 100644 --- a/src/Services/Identity/Identity.API/Properties/launchSettings.json +++ b/src/Services/Identity/Identity.API/Properties/launchSettings.json @@ -3,8 +3,8 @@ "windowsAuthentication": false, "anonymousAuthentication": true, "iisExpress": { - "applicationUrl": "http://localhost:54010/", - "sslPort": 0 + "applicationUrl": "http://localhost:49318/", + "sslPort": 44350 } }, "profiles": { diff --git a/src/Services/Ordering/Ordering.API/Dockerfile b/src/Services/Ordering/Ordering.API/Dockerfile index 905d3bcd0..560a60c5c 100644 --- a/src/Services/Ordering/Ordering.API/Dockerfile +++ b/src/Services/Ordering/Ordering.API/Dockerfile @@ -24,6 +24,7 @@ COPY "Services/Basket/Basket.UnitTests/Basket.UnitTests.csproj" "Services/Basket COPY "Services/Catalog/Catalog.API/Catalog.API.csproj" "Services/Catalog/Catalog.API/Catalog.API.csproj" COPY "Services/Catalog/Catalog.FunctionalTests/Catalog.FunctionalTests.csproj" "Services/Catalog/Catalog.FunctionalTests/Catalog.FunctionalTests.csproj" COPY "Services/Catalog/Catalog.UnitTests/Catalog.UnitTests.csproj" "Services/Catalog/Catalog.UnitTests/Catalog.UnitTests.csproj" +COPY "Services/Coupon/Coupon.API/Coupon.API.csproj" "Services/Coupon/Coupon.API/Coupon.API.csproj" COPY "Services/Identity/Identity.API/Identity.API.csproj" "Services/Identity/Identity.API/Identity.API.csproj" COPY "Services/Ordering/Ordering.API/Ordering.API.csproj" "Services/Ordering/Ordering.API/Ordering.API.csproj" COPY "Services/Ordering/Ordering.BackgroundTasks/Ordering.BackgroundTasks.csproj" "Services/Ordering/Ordering.BackgroundTasks/Ordering.BackgroundTasks.csproj" diff --git a/src/Services/Ordering/Ordering.BackgroundTasks/Dockerfile b/src/Services/Ordering/Ordering.BackgroundTasks/Dockerfile index 0a5ce8f0c..d2923dc37 100644 --- a/src/Services/Ordering/Ordering.BackgroundTasks/Dockerfile +++ b/src/Services/Ordering/Ordering.BackgroundTasks/Dockerfile @@ -24,6 +24,7 @@ COPY "Services/Basket/Basket.UnitTests/Basket.UnitTests.csproj" "Services/Basket COPY "Services/Catalog/Catalog.API/Catalog.API.csproj" "Services/Catalog/Catalog.API/Catalog.API.csproj" COPY "Services/Catalog/Catalog.FunctionalTests/Catalog.FunctionalTests.csproj" "Services/Catalog/Catalog.FunctionalTests/Catalog.FunctionalTests.csproj" COPY "Services/Catalog/Catalog.UnitTests/Catalog.UnitTests.csproj" "Services/Catalog/Catalog.UnitTests/Catalog.UnitTests.csproj" +COPY "Services/Coupon/Coupon.API/Coupon.API.csproj" "Services/Coupon/Coupon.API/Coupon.API.csproj" COPY "Services/Identity/Identity.API/Identity.API.csproj" "Services/Identity/Identity.API/Identity.API.csproj" COPY "Services/Ordering/Ordering.API/Ordering.API.csproj" "Services/Ordering/Ordering.API/Ordering.API.csproj" COPY "Services/Ordering/Ordering.BackgroundTasks/Ordering.BackgroundTasks.csproj" "Services/Ordering/Ordering.BackgroundTasks/Ordering.BackgroundTasks.csproj" diff --git a/src/Services/Ordering/Ordering.SignalrHub/Dockerfile b/src/Services/Ordering/Ordering.SignalrHub/Dockerfile index 33771bf1f..f51b5d0cf 100644 --- a/src/Services/Ordering/Ordering.SignalrHub/Dockerfile +++ b/src/Services/Ordering/Ordering.SignalrHub/Dockerfile @@ -24,6 +24,7 @@ COPY "Services/Basket/Basket.UnitTests/Basket.UnitTests.csproj" "Services/Basket COPY "Services/Catalog/Catalog.API/Catalog.API.csproj" "Services/Catalog/Catalog.API/Catalog.API.csproj" COPY "Services/Catalog/Catalog.FunctionalTests/Catalog.FunctionalTests.csproj" "Services/Catalog/Catalog.FunctionalTests/Catalog.FunctionalTests.csproj" COPY "Services/Catalog/Catalog.UnitTests/Catalog.UnitTests.csproj" "Services/Catalog/Catalog.UnitTests/Catalog.UnitTests.csproj" +COPY "Services/Coupon/Coupon.API/Coupon.API.csproj" "Services/Coupon/Coupon.API/Coupon.API.csproj" COPY "Services/Identity/Identity.API/Identity.API.csproj" "Services/Identity/Identity.API/Identity.API.csproj" COPY "Services/Ordering/Ordering.API/Ordering.API.csproj" "Services/Ordering/Ordering.API/Ordering.API.csproj" COPY "Services/Ordering/Ordering.BackgroundTasks/Ordering.BackgroundTasks.csproj" "Services/Ordering/Ordering.BackgroundTasks/Ordering.BackgroundTasks.csproj" diff --git a/src/Services/Payment/Payment.API/Dockerfile b/src/Services/Payment/Payment.API/Dockerfile index e91ed767b..caefc955e 100644 --- a/src/Services/Payment/Payment.API/Dockerfile +++ b/src/Services/Payment/Payment.API/Dockerfile @@ -24,6 +24,7 @@ COPY "Services/Basket/Basket.UnitTests/Basket.UnitTests.csproj" "Services/Basket COPY "Services/Catalog/Catalog.API/Catalog.API.csproj" "Services/Catalog/Catalog.API/Catalog.API.csproj" COPY "Services/Catalog/Catalog.FunctionalTests/Catalog.FunctionalTests.csproj" "Services/Catalog/Catalog.FunctionalTests/Catalog.FunctionalTests.csproj" COPY "Services/Catalog/Catalog.UnitTests/Catalog.UnitTests.csproj" "Services/Catalog/Catalog.UnitTests/Catalog.UnitTests.csproj" +COPY "Services/Coupon/Coupon.API/Coupon.API.csproj" "Services/Coupon/Coupon.API/Coupon.API.csproj" COPY "Services/Identity/Identity.API/Identity.API.csproj" "Services/Identity/Identity.API/Identity.API.csproj" COPY "Services/Ordering/Ordering.API/Ordering.API.csproj" "Services/Ordering/Ordering.API/Ordering.API.csproj" COPY "Services/Ordering/Ordering.BackgroundTasks/Ordering.BackgroundTasks.csproj" "Services/Ordering/Ordering.BackgroundTasks/Ordering.BackgroundTasks.csproj" diff --git a/src/Services/Webhooks/Webhooks.API/Dockerfile b/src/Services/Webhooks/Webhooks.API/Dockerfile index b5fb88684..54186fd5f 100644 --- a/src/Services/Webhooks/Webhooks.API/Dockerfile +++ b/src/Services/Webhooks/Webhooks.API/Dockerfile @@ -24,6 +24,7 @@ COPY "Services/Basket/Basket.UnitTests/Basket.UnitTests.csproj" "Services/Basket COPY "Services/Catalog/Catalog.API/Catalog.API.csproj" "Services/Catalog/Catalog.API/Catalog.API.csproj" COPY "Services/Catalog/Catalog.FunctionalTests/Catalog.FunctionalTests.csproj" "Services/Catalog/Catalog.FunctionalTests/Catalog.FunctionalTests.csproj" COPY "Services/Catalog/Catalog.UnitTests/Catalog.UnitTests.csproj" "Services/Catalog/Catalog.UnitTests/Catalog.UnitTests.csproj" +COPY "Services/Coupon/Coupon.API/Coupon.API.csproj" "Services/Coupon/Coupon.API/Coupon.API.csproj" COPY "Services/Identity/Identity.API/Identity.API.csproj" "Services/Identity/Identity.API/Identity.API.csproj" COPY "Services/Ordering/Ordering.API/Ordering.API.csproj" "Services/Ordering/Ordering.API/Ordering.API.csproj" COPY "Services/Ordering/Ordering.BackgroundTasks/Ordering.BackgroundTasks.csproj" "Services/Ordering/Ordering.BackgroundTasks/Ordering.BackgroundTasks.csproj" diff --git a/src/Web/WebMVC/Dockerfile b/src/Web/WebMVC/Dockerfile index 470de25d2..d70dfe1d0 100644 --- a/src/Web/WebMVC/Dockerfile +++ b/src/Web/WebMVC/Dockerfile @@ -24,6 +24,7 @@ COPY "Services/Basket/Basket.UnitTests/Basket.UnitTests.csproj" "Services/Basket COPY "Services/Catalog/Catalog.API/Catalog.API.csproj" "Services/Catalog/Catalog.API/Catalog.API.csproj" COPY "Services/Catalog/Catalog.FunctionalTests/Catalog.FunctionalTests.csproj" "Services/Catalog/Catalog.FunctionalTests/Catalog.FunctionalTests.csproj" COPY "Services/Catalog/Catalog.UnitTests/Catalog.UnitTests.csproj" "Services/Catalog/Catalog.UnitTests/Catalog.UnitTests.csproj" +COPY "Services/Coupon/Coupon.API/Coupon.API.csproj" "Services/Coupon/Coupon.API/Coupon.API.csproj" COPY "Services/Identity/Identity.API/Identity.API.csproj" "Services/Identity/Identity.API/Identity.API.csproj" COPY "Services/Ordering/Ordering.API/Ordering.API.csproj" "Services/Ordering/Ordering.API/Ordering.API.csproj" COPY "Services/Ordering/Ordering.BackgroundTasks/Ordering.BackgroundTasks.csproj" "Services/Ordering/Ordering.BackgroundTasks/Ordering.BackgroundTasks.csproj" diff --git a/src/Web/WebSPA/Client/src/modules/basket/basket.service.ts b/src/Web/WebSPA/Client/src/modules/basket/basket.service.ts index b14ff3e9b..78b975727 100644 --- a/src/Web/WebSPA/Client/src/modules/basket/basket.service.ts +++ b/src/Web/WebSPA/Client/src/modules/basket/basket.service.ts @@ -13,6 +13,7 @@ import { StorageService } from '../shared/services/storage.service'; import { Observable, Observer, Subject } from 'rxjs'; import { map, catchError, tap } from 'rxjs/operators'; +import { ICoupon } from '../shared/models/coupon.model'; @Injectable() export class BasketService { @@ -95,6 +96,27 @@ export class BasketService { })); } + checkValidationCoupon(code: string): Observable { + let url = this.purchaseUrl + `/cp/api/v1/coupon/${code}`; + + return this.service + .get(url) + .pipe(map((response) => + { + console.log(`Coupon: ${response.json()} (${response.ok})`); + var item = response.json[0]; + + if (response.ok) { + item.message = "Valid coupon"; + } + else { + item.message = "The coupon is not valid or has already been used"; + } + + return item; + })); + } + mapBasketInfoCheckout(order: IOrder): IBasketCheckout { let basketCheckout = {}; diff --git a/src/Web/WebSPA/Client/src/modules/orders/orders-detail/orders-detail.component.html b/src/Web/WebSPA/Client/src/modules/orders/orders-detail/orders-detail.component.html index e0ea4f722..17c914337 100644 --- a/src/Web/WebSPA/Client/src/modules/orders/orders-detail/orders-detail.component.html +++ b/src/Web/WebSPA/Client/src/modules/orders/orders-detail/orders-detail.component.html @@ -34,6 +34,15 @@ +
+
Subtotal
+
${{order.subtotal | number:'.2-2'}}
+
+
+
{{order.coupon}}
+
- ${{order.discount | number:'.2-2'}}
+
--> +
Total
${{order.total | number:'.2-2'}}
diff --git a/src/Web/WebSPA/Client/src/modules/orders/orders-new/orders-new.component.html b/src/Web/WebSPA/Client/src/modules/orders/orders-new/orders-new.component.html index 18311597a..3b54685fd 100644 --- a/src/Web/WebSPA/Client/src/modules/orders/orders-new/orders-new.component.html +++ b/src/Web/WebSPA/Client/src/modules/orders/orders-new/orders-new.component.html @@ -84,6 +84,38 @@
+
+
Subtotal
+
${{order.total | number:'.2-2'}}
+
+ +
+
+
+
Have a discount code?
+
+ + +
+
{{couponValidationMessage}}
+
+
+
+
{{coupon?.code}}
+
+
-${{coupon?.discount | number:'.2-2'}}
+
+
+
+
Total
${{ order.total | number:'.2-2'}}
diff --git a/src/Web/WebSPA/Client/src/modules/orders/orders-new/orders-new.component.ts b/src/Web/WebSPA/Client/src/modules/orders/orders-new/orders-new.component.ts index ff84b7638..93b0bc9f2 100644 --- a/src/Web/WebSPA/Client/src/modules/orders/orders-new/orders-new.component.ts +++ b/src/Web/WebSPA/Client/src/modules/orders/orders-new/orders-new.component.ts @@ -6,7 +6,7 @@ import { OrdersService } from '../orders.service'; import { BasketService } from '../../basket/basket.service'; import { IOrder } from '../../shared/models/order.model'; import { BasketWrapperService } from '../../shared/services/basket.wrapper.service'; - +import { ICoupon } from '../../shared/models/coupon.model'; import { FormGroup, FormBuilder, Validators } from '@angular/forms'; import { Router } from '@angular/router'; @@ -19,7 +19,9 @@ export class OrdersNewComponent implements OnInit { newOrderForm: FormGroup; // new order form isOrderProcessing: boolean; errorReceived: boolean; + coupon: ICoupon; order: IOrder; + couponValidationMessage: string; constructor(private orderService: OrdersService, private basketService: BasketService, fb: FormBuilder, private router: Router) { // Obtain user profile information @@ -39,6 +41,23 @@ export class OrdersNewComponent implements OnInit { ngOnInit() { } + keyDownValidationCoupon(event: KeyboardEvent, discountCode: string) { + if(event.keyCode === 13) { + event.preventDefault(); + this.checkValidationCoupon(discountCode); + } + } + + checkValidationCoupon(discountCode: string) { + this.couponValidationMessage = null; + this.coupon = null; + this.orderService + .checkValidationCoupon(discountCode) + .subscribe( + coupon => this.coupon = coupon, + error => this.couponValidationMessage = 'The coupon is not valid or it\'s been redeemed already!' ); + } + submitForm(value: any) { this.order.street = this.newOrderForm.controls['street'].value; this.order.city = this.newOrderForm.controls['city'].value; @@ -49,6 +68,14 @@ export class OrdersNewComponent implements OnInit { this.order.cardholdername = this.newOrderForm.controls['cardholdername'].value; this.order.cardexpiration = new Date(20 + this.newOrderForm.controls['expirationdate'].value.split('/')[1], this.newOrderForm.controls['expirationdate'].value.split('/')[0]); this.order.cardsecuritynumber = this.newOrderForm.controls['securitycode'].value; + + if (this.coupon) { + console.log(`Coupon: ${this.coupon.code} (${this.coupon.discount})`); + + this.order.coupon = this.coupon.code; + this.order.discount = this.coupon.discount; + } + let basketCheckout = this.basketService.mapBasketInfoCheckout(this.order); this.basketService.setBasketCheckout(basketCheckout) .pipe(catchError((errMessage) => { diff --git a/src/Web/WebSPA/Client/src/modules/orders/orders.service.ts b/src/Web/WebSPA/Client/src/modules/orders/orders.service.ts index e81d6da64..1b9508e62 100644 --- a/src/Web/WebSPA/Client/src/modules/orders/orders.service.ts +++ b/src/Web/WebSPA/Client/src/modules/orders/orders.service.ts @@ -10,6 +10,7 @@ import { BasketWrapperService } from '../shared/services/basket.wrapper.service' import { Observable } from 'rxjs'; import { tap, map } from 'rxjs/operators'; +import { ICoupon } from '../shared/models/coupon.model'; @Injectable() export class OrdersService { @@ -48,6 +49,14 @@ export class OrdersService { })); } + checkValidationCoupon(code: string): Observable { + let url = this.ordersUrl + `/cp/api/v1/coupon/${code}`; + + return this.service.get(url).pipe(tap((response: any) => { + return response; + })); + } + mapOrderAndIdentityInfoNewOrder(): IOrder { let order = {}; let basket = this.basketService.basket; diff --git a/src/Web/WebSPA/Client/src/modules/shared/models/coupon.model.ts b/src/Web/WebSPA/Client/src/modules/shared/models/coupon.model.ts new file mode 100644 index 000000000..e72f5f8f7 --- /dev/null +++ b/src/Web/WebSPA/Client/src/modules/shared/models/coupon.model.ts @@ -0,0 +1,5 @@ +export interface ICoupon { + discount: number; + code: string; + message: string +} \ No newline at end of file diff --git a/src/Web/WebSPA/Client/src/modules/shared/models/order-detail.model.ts b/src/Web/WebSPA/Client/src/modules/shared/models/order-detail.model.ts index 5631d8807..b5cec7e4d 100644 --- a/src/Web/WebSPA/Client/src/modules/shared/models/order-detail.model.ts +++ b/src/Web/WebSPA/Client/src/modules/shared/models/order-detail.model.ts @@ -10,6 +10,9 @@ export interface IOrderDetail { state: string; zipcode: string; country: number; + subtotal: number; + coupon: string; + discount: number; total: number; orderitems: IOrderItem[]; } diff --git a/src/Web/WebSPA/Client/src/modules/shared/models/order.model.ts b/src/Web/WebSPA/Client/src/modules/shared/models/order.model.ts index 9780f40ad..220f05bde 100644 --- a/src/Web/WebSPA/Client/src/modules/shared/models/order.model.ts +++ b/src/Web/WebSPA/Client/src/modules/shared/models/order.model.ts @@ -14,6 +14,9 @@ export interface IOrder { cardtypeid: number; buyer: string; ordernumber: string; + subtotal: number, + coupon: string; + discount: number; total: number; orderItems: IOrderItem[]; } diff --git a/src/Web/WebSPA/Dockerfile b/src/Web/WebSPA/Dockerfile index 00ff8b3b5..ceca4c045 100644 --- a/src/Web/WebSPA/Dockerfile +++ b/src/Web/WebSPA/Dockerfile @@ -35,6 +35,7 @@ COPY "Services/Basket/Basket.UnitTests/Basket.UnitTests.csproj" "Services/Basket COPY "Services/Catalog/Catalog.API/Catalog.API.csproj" "Services/Catalog/Catalog.API/Catalog.API.csproj" COPY "Services/Catalog/Catalog.FunctionalTests/Catalog.FunctionalTests.csproj" "Services/Catalog/Catalog.FunctionalTests/Catalog.FunctionalTests.csproj" COPY "Services/Catalog/Catalog.UnitTests/Catalog.UnitTests.csproj" "Services/Catalog/Catalog.UnitTests/Catalog.UnitTests.csproj" +COPY "Services/Coupon/Coupon.API/Coupon.API.csproj" "Services/Coupon/Coupon.API/Coupon.API.csproj" COPY "Services/Identity/Identity.API/Identity.API.csproj" "Services/Identity/Identity.API/Identity.API.csproj" COPY "Services/Ordering/Ordering.API/Ordering.API.csproj" "Services/Ordering/Ordering.API/Ordering.API.csproj" COPY "Services/Ordering/Ordering.BackgroundTasks/Ordering.BackgroundTasks.csproj" "Services/Ordering/Ordering.BackgroundTasks/Ordering.BackgroundTasks.csproj" diff --git a/src/Web/WebStatus/Dockerfile b/src/Web/WebStatus/Dockerfile index 47ecbd9e4..0f3ffe462 100644 --- a/src/Web/WebStatus/Dockerfile +++ b/src/Web/WebStatus/Dockerfile @@ -24,6 +24,7 @@ COPY "Services/Basket/Basket.UnitTests/Basket.UnitTests.csproj" "Services/Basket COPY "Services/Catalog/Catalog.API/Catalog.API.csproj" "Services/Catalog/Catalog.API/Catalog.API.csproj" COPY "Services/Catalog/Catalog.FunctionalTests/Catalog.FunctionalTests.csproj" "Services/Catalog/Catalog.FunctionalTests/Catalog.FunctionalTests.csproj" COPY "Services/Catalog/Catalog.UnitTests/Catalog.UnitTests.csproj" "Services/Catalog/Catalog.UnitTests/Catalog.UnitTests.csproj" +COPY "Services/Coupon/Coupon.API/Coupon.API.csproj" "Services/Coupon/Coupon.API/Coupon.API.csproj" COPY "Services/Identity/Identity.API/Identity.API.csproj" "Services/Identity/Identity.API/Identity.API.csproj" COPY "Services/Ordering/Ordering.API/Ordering.API.csproj" "Services/Ordering/Ordering.API/Ordering.API.csproj" COPY "Services/Ordering/Ordering.BackgroundTasks/Ordering.BackgroundTasks.csproj" "Services/Ordering/Ordering.BackgroundTasks/Ordering.BackgroundTasks.csproj" diff --git a/src/Web/WebhookClient/Dockerfile b/src/Web/WebhookClient/Dockerfile index c25957029..14131701c 100644 --- a/src/Web/WebhookClient/Dockerfile +++ b/src/Web/WebhookClient/Dockerfile @@ -25,6 +25,7 @@ COPY "Services/Basket/Basket.UnitTests/Basket.UnitTests.csproj" "Services/Basket COPY "Services/Catalog/Catalog.API/Catalog.API.csproj" "Services/Catalog/Catalog.API/Catalog.API.csproj" COPY "Services/Catalog/Catalog.FunctionalTests/Catalog.FunctionalTests.csproj" "Services/Catalog/Catalog.FunctionalTests/Catalog.FunctionalTests.csproj" COPY "Services/Catalog/Catalog.UnitTests/Catalog.UnitTests.csproj" "Services/Catalog/Catalog.UnitTests/Catalog.UnitTests.csproj" +COPY "Services/Coupon/Coupon.API/Coupon.API.csproj" "Services/Coupon/Coupon.API/Coupon.API.csproj" COPY "Services/Identity/Identity.API/Identity.API.csproj" "Services/Identity/Identity.API/Identity.API.csproj" COPY "Services/Ordering/Ordering.API/Ordering.API.csproj" "Services/Ordering/Ordering.API/Ordering.API.csproj" COPY "Services/Ordering/Ordering.BackgroundTasks/Ordering.BackgroundTasks.csproj" "Services/Ordering/Ordering.BackgroundTasks/Ordering.BackgroundTasks.csproj" diff --git a/src/docker-compose.yml b/src/docker-compose.yml index 3ae5d431a..17837af68 100644 --- a/src/docker-compose.yml +++ b/src/docker-compose.yml @@ -44,6 +44,14 @@ services: - sqldata - rabbitmq + coupon-api: + image: ${REGISTRY:-eshop}/coupon.api:${PLATFORM:-linux}-${TAG:-latest} + build: + context: . + dockerfile: Services/Coupon/Coupon.API/Dockerfile + depends_on: + - nosqldata + ordering-api: image: ${REGISTRY:-eshop}/ordering.api:${PLATFORM:-linux}-${TAG:-latest} build: diff --git a/src/eShopOnContainers-ServicesAndWebApps.sln b/src/eShopOnContainers-ServicesAndWebApps.sln index 446614fc0..cfbb6e5fe 100644 --- a/src/eShopOnContainers-ServicesAndWebApps.sln +++ b/src/eShopOnContainers-ServicesAndWebApps.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.29020.237 +# Visual Studio Version 17 +VisualStudioVersion = 17.2.32929.388 MinimumVisualStudioVersion = 10.0.40219.1 Project("{E53339B2-1760-4266-BCC7-CA923CBCF16C}") = "docker-compose", "docker-compose.dcproj", "{FEA0C318-FFED-4D39-8781-265718CA43DD}" EndProject @@ -124,6 +124,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{373D8AA1 EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EventBus.Tests", "BuildingBlocks\EventBus\EventBus.Tests\EventBus.Tests.csproj", "{95D735BE-2899-4495-BE3F-2600E93B4E3C}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Coupon", "Coupon", "{71D3CD0A-5583-4F82-99D6-5035BF0EE2C0}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Coupon.API", "Services\Coupon\Coupon.API\Coupon.API.csproj", "{53D3EE9A-EA1E-46A7-9B62-1D19E6FE2083}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Ad-Hoc|Any CPU = Ad-Hoc|Any CPU @@ -1530,6 +1534,54 @@ Global {95D735BE-2899-4495-BE3F-2600E93B4E3C}.Release|x64.Build.0 = Release|Any CPU {95D735BE-2899-4495-BE3F-2600E93B4E3C}.Release|x86.ActiveCfg = Release|Any CPU {95D735BE-2899-4495-BE3F-2600E93B4E3C}.Release|x86.Build.0 = Release|Any CPU + {53D3EE9A-EA1E-46A7-9B62-1D19E6FE2083}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU + {53D3EE9A-EA1E-46A7-9B62-1D19E6FE2083}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU + {53D3EE9A-EA1E-46A7-9B62-1D19E6FE2083}.Ad-Hoc|ARM.ActiveCfg = Debug|Any CPU + {53D3EE9A-EA1E-46A7-9B62-1D19E6FE2083}.Ad-Hoc|ARM.Build.0 = Debug|Any CPU + {53D3EE9A-EA1E-46A7-9B62-1D19E6FE2083}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU + {53D3EE9A-EA1E-46A7-9B62-1D19E6FE2083}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU + {53D3EE9A-EA1E-46A7-9B62-1D19E6FE2083}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {53D3EE9A-EA1E-46A7-9B62-1D19E6FE2083}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU + {53D3EE9A-EA1E-46A7-9B62-1D19E6FE2083}.Ad-Hoc|x64.ActiveCfg = Debug|Any CPU + {53D3EE9A-EA1E-46A7-9B62-1D19E6FE2083}.Ad-Hoc|x64.Build.0 = Debug|Any CPU + {53D3EE9A-EA1E-46A7-9B62-1D19E6FE2083}.Ad-Hoc|x86.ActiveCfg = Debug|Any CPU + {53D3EE9A-EA1E-46A7-9B62-1D19E6FE2083}.Ad-Hoc|x86.Build.0 = Debug|Any CPU + {53D3EE9A-EA1E-46A7-9B62-1D19E6FE2083}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU + {53D3EE9A-EA1E-46A7-9B62-1D19E6FE2083}.AppStore|Any CPU.Build.0 = Debug|Any CPU + {53D3EE9A-EA1E-46A7-9B62-1D19E6FE2083}.AppStore|ARM.ActiveCfg = Debug|Any CPU + {53D3EE9A-EA1E-46A7-9B62-1D19E6FE2083}.AppStore|ARM.Build.0 = Debug|Any CPU + {53D3EE9A-EA1E-46A7-9B62-1D19E6FE2083}.AppStore|iPhone.ActiveCfg = Debug|Any CPU + {53D3EE9A-EA1E-46A7-9B62-1D19E6FE2083}.AppStore|iPhone.Build.0 = Debug|Any CPU + {53D3EE9A-EA1E-46A7-9B62-1D19E6FE2083}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {53D3EE9A-EA1E-46A7-9B62-1D19E6FE2083}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU + {53D3EE9A-EA1E-46A7-9B62-1D19E6FE2083}.AppStore|x64.ActiveCfg = Debug|Any CPU + {53D3EE9A-EA1E-46A7-9B62-1D19E6FE2083}.AppStore|x64.Build.0 = Debug|Any CPU + {53D3EE9A-EA1E-46A7-9B62-1D19E6FE2083}.AppStore|x86.ActiveCfg = Debug|Any CPU + {53D3EE9A-EA1E-46A7-9B62-1D19E6FE2083}.AppStore|x86.Build.0 = Debug|Any CPU + {53D3EE9A-EA1E-46A7-9B62-1D19E6FE2083}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {53D3EE9A-EA1E-46A7-9B62-1D19E6FE2083}.Debug|Any CPU.Build.0 = Debug|Any CPU + {53D3EE9A-EA1E-46A7-9B62-1D19E6FE2083}.Debug|ARM.ActiveCfg = Debug|Any CPU + {53D3EE9A-EA1E-46A7-9B62-1D19E6FE2083}.Debug|ARM.Build.0 = Debug|Any CPU + {53D3EE9A-EA1E-46A7-9B62-1D19E6FE2083}.Debug|iPhone.ActiveCfg = Debug|Any CPU + {53D3EE9A-EA1E-46A7-9B62-1D19E6FE2083}.Debug|iPhone.Build.0 = Debug|Any CPU + {53D3EE9A-EA1E-46A7-9B62-1D19E6FE2083}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {53D3EE9A-EA1E-46A7-9B62-1D19E6FE2083}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU + {53D3EE9A-EA1E-46A7-9B62-1D19E6FE2083}.Debug|x64.ActiveCfg = Debug|Any CPU + {53D3EE9A-EA1E-46A7-9B62-1D19E6FE2083}.Debug|x64.Build.0 = Debug|Any CPU + {53D3EE9A-EA1E-46A7-9B62-1D19E6FE2083}.Debug|x86.ActiveCfg = Debug|Any CPU + {53D3EE9A-EA1E-46A7-9B62-1D19E6FE2083}.Debug|x86.Build.0 = Debug|Any CPU + {53D3EE9A-EA1E-46A7-9B62-1D19E6FE2083}.Release|Any CPU.ActiveCfg = Release|Any CPU + {53D3EE9A-EA1E-46A7-9B62-1D19E6FE2083}.Release|Any CPU.Build.0 = Release|Any CPU + {53D3EE9A-EA1E-46A7-9B62-1D19E6FE2083}.Release|ARM.ActiveCfg = Release|Any CPU + {53D3EE9A-EA1E-46A7-9B62-1D19E6FE2083}.Release|ARM.Build.0 = Release|Any CPU + {53D3EE9A-EA1E-46A7-9B62-1D19E6FE2083}.Release|iPhone.ActiveCfg = Release|Any CPU + {53D3EE9A-EA1E-46A7-9B62-1D19E6FE2083}.Release|iPhone.Build.0 = Release|Any CPU + {53D3EE9A-EA1E-46A7-9B62-1D19E6FE2083}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU + {53D3EE9A-EA1E-46A7-9B62-1D19E6FE2083}.Release|iPhoneSimulator.Build.0 = Release|Any CPU + {53D3EE9A-EA1E-46A7-9B62-1D19E6FE2083}.Release|x64.ActiveCfg = Release|Any CPU + {53D3EE9A-EA1E-46A7-9B62-1D19E6FE2083}.Release|x64.Build.0 = Release|Any CPU + {53D3EE9A-EA1E-46A7-9B62-1D19E6FE2083}.Release|x86.ActiveCfg = Release|Any CPU + {53D3EE9A-EA1E-46A7-9B62-1D19E6FE2083}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -1588,6 +1640,8 @@ Global {B62E859F-825E-4C8B-93EC-5966EACFD026} = {798BFC44-2CCD-45FA-B37A-5173B03C2B30} {373D8AA1-36BE-49EC-89F0-6CB736666285} = {807BB76E-B2BB-47A2-A57B-3D1B20FF5E7F} {95D735BE-2899-4495-BE3F-2600E93B4E3C} = {373D8AA1-36BE-49EC-89F0-6CB736666285} + {71D3CD0A-5583-4F82-99D6-5035BF0EE2C0} = {91CF7717-08AB-4E65-B10E-0B426F01E2E8} + {53D3EE9A-EA1E-46A7-9B62-1D19E6FE2083} = {71D3CD0A-5583-4F82-99D6-5035BF0EE2C0} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {25728519-5F0F-4973-8A64-0A81EB4EA8D9}