Skip to content

Commit

Permalink
Merge pull request #7 from rarimo/feature/external-fulfillment
Browse files Browse the repository at this point in the history
Feature: event fulfillment from external services
  • Loading branch information
violog authored Feb 28, 2024
2 parents ab91091 + 8cbdbb4 commit 38b030e
Show file tree
Hide file tree
Showing 18 changed files with 381 additions and 105 deletions.
48 changes: 26 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,24 +6,39 @@ Core service of Rarimo Points System

## Install

```
git clone github.com/rarimo/rarime-points-svc
cd rarime-points-svc
go build main.go
export KV_VIPER_FILE=./config.yaml
./main migrate up
./main run service
```
```
git clone github.com/rarimo/rarime-points-svc
cd rarime-points-svc
go build main.go
export KV_VIPER_FILE=./config.yaml
./main migrate up
./main run service
```

## API documentation

[Online docs](https://rarimo.github.io/rarime-points-svc/) are available.

All endpoints from docs MUST be publicly accessible.

### Private endpoints

## Documentation
Private endpoints are not documented and MUST only be accessible within the
internal network. They do not require authorization in order to simplify back-end
interactions with Points service. Package [connector](./pkg/connector) provides
functionality to interact with these endpoints.

The path for internal endpoints is `/integrations/rarime-points-svc/v1/private/*`.

### Local build

We do use openapi:json standard for API. We use swagger for documenting our API.

To open online documentation, go to [swagger editor](http://localhost:8080/swagger-editor/) here is how you can start it
```
cd docs
npm install
npm start
npm run start
```
To build documentation use `npm run build` command,
that will create open-api documentation in `web_deploy` folder.
Expand All @@ -34,25 +49,14 @@ use `./generate.sh --help` to see all available options.
Note: if you are using Gitlab for building project `docs/spec/paths` folder must not be
empty, otherwise only `Build and Publish` job will be passed.

## Running from docker

Make sure that docker installed.

use `docker run ` with `-p 8080:80` to expose port 80 to 8080

```
docker build -t github.com/rarimo/rarime-points-svc .
docker run -e KV_VIPER_FILE=/config.yaml github.com/rarimo/rarime-points-svc
```

## Running from Source

* Run dependencies, based on config example
* Set up environment value with config file path `KV_VIPER_FILE=./config.yaml`
* Provide valid config file
* Launch the service with `migrate up` command to create database schema
* Launch the service with `run service` command


### Database
For services, we do use ***PostgresSQL*** database.
You can [install it locally](https://www.postgresql.org/download/) or use [docker image](https://hub.docker.com/_/postgres/).
15 changes: 4 additions & 11 deletions internal/assets/migrations/001_initial.sql
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,9 @@ CREATE TABLE IF NOT EXISTS events
created_at integer NOT NULL default EXTRACT('EPOCH' FROM NOW()),
updated_at integer NOT NULL default EXTRACT('EPOCH' FROM NOW()),
meta jsonb,
points_amount integer
points_amount integer,
external_id text,
CONSTRAINT unique_external_id UNIQUE (user_did, type, external_id)
);

CREATE INDEX IF NOT EXISTS events_user_did_index ON events using btree (user_did);
Expand All @@ -59,18 +61,9 @@ CREATE TABLE IF NOT EXISTS withdrawals
CREATE INDEX IF NOT EXISTS withdrawals_user_did_index ON withdrawals using btree (user_did);

-- +migrate Down
DROP INDEX IF EXISTS withdrawals_user_did_index;
DROP TABLE IF EXISTS withdrawals;

DROP TRIGGER IF EXISTS set_updated_at ON events;
DROP INDEX IF EXISTS events_type_index;
DROP INDEX IF EXISTS events_user_did_index;
DROP INDEX IF EXISTS events_updated_at_index;
DROP TABLE IF EXISTS events;
DROP TYPE IF EXISTS event_status;

DROP TRIGGER IF EXISTS set_updated_at ON balances;
DROP INDEX IF EXISTS balances_amount_index;
DROP TABLE IF EXISTS balances;

DROP TYPE IF EXISTS event_status;
DROP FUNCTION IF EXISTS trigger_set_updated_at();
19 changes: 11 additions & 8 deletions internal/data/events.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package data

import (
"database/sql"
"encoding/json"

"gitlab.com/distributed_lab/kit/pgdb"
Expand All @@ -19,14 +20,15 @@ func (s EventStatus) String() string {
}

type Event struct {
ID string `db:"id"`
UserDID string `db:"user_did"`
Type string `db:"type"`
Status EventStatus `db:"status"`
CreatedAt int32 `db:"created_at"`
UpdatedAt int32 `db:"updated_at"`
Meta Jsonb `db:"meta"`
PointsAmount *int64 `db:"points_amount"`
ID string `db:"id"`
UserDID string `db:"user_did"`
Type string `db:"type"`
Status EventStatus `db:"status"`
CreatedAt int32 `db:"created_at"`
UpdatedAt int32 `db:"updated_at"`
Meta Jsonb `db:"meta"`
PointsAmount *int64 `db:"points_amount"`
ExternalID sql.NullString `db:"external_id"` // hidden from client
}

// ReopenableEvent is a pair that is sufficient to build a new open event with a specific type for a user
Expand Down Expand Up @@ -60,4 +62,5 @@ type EventsQ interface {
FilterByStatus(...EventStatus) EventsQ
FilterByType(...string) EventsQ
FilterByUpdatedAtBefore(int64) EventsQ
FilterByExternalID(string) EventsQ
}
8 changes: 6 additions & 2 deletions internal/data/pg/events.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,13 +44,13 @@ func (q *events) Insert(events ...data.Event) error {
}

stmt := squirrel.Insert(eventsTable).
Columns("user_did", "type", "status", "meta", "points_amount")
Columns("user_did", "type", "status", "meta", "points_amount", "external_id")
for _, event := range events {
var meta any
if len(event.Meta) != 0 {
meta = event.Meta
}
stmt = stmt.Values(event.UserDID, event.Type, event.Status, meta, event.PointsAmount)
stmt = stmt.Values(event.UserDID, event.Type, event.Status, meta, event.PointsAmount, event.ExternalID)
}

if err := q.db.Exec(stmt); err != nil {
Expand Down Expand Up @@ -204,6 +204,10 @@ func (q *events) FilterByType(types ...string) data.EventsQ {
return q.applyCondition(squirrel.Eq{"type": types})
}

func (q *events) FilterByExternalID(id string) data.EventsQ {
return q.applyCondition(squirrel.Eq{"external_id": id})
}

func (q *events) FilterByUpdatedAtBefore(unix int64) data.EventsQ {
return q.applyCondition(squirrel.Lt{"updated_at": unix})
}
Expand Down
85 changes: 49 additions & 36 deletions internal/service/handlers/create_balance.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ func CreateBalance(w http.ResponseWriter, r *http.Request) {
return
}

var referredBy sql.NullString
var referredBy string
if attr := req.Data.Attributes; attr != nil {
referrer, err := BalancesQ(r).FilterByReferralID(attr.ReferredBy).Get()
if err != nil {
Expand All @@ -54,50 +54,20 @@ func CreateBalance(w http.ResponseWriter, r *http.Request) {
ape.RenderErr(w, problems.NotFound())
return
}
referredBy = sql.NullString{String: attr.ReferredBy, Valid: true}
referredBy = attr.ReferredBy
}

events := EventTypes(r).PrepareEvents(did, evtypes.FilterNotOpenable)
if referredBy.Valid {
evType := EventTypes(r).Get(evtypes.TypeBeReferred, evtypes.FilterInactive)
if evType != nil {
events = append(events, data.Event{
UserDID: did,
Type: evtypes.TypeBeReferred,
Status: data.EventFulfilled,
})
} else {
Log(r).Debug("Referral event is disabled or expired, skipping it")
}
}

err = EventsQ(r).Transaction(func() error {
err = BalancesQ(r).Insert(data.Balance{
DID: did,
ReferralID: referralid.New(did),
ReferredBy: referredBy,
})
if err != nil {
return fmt.Errorf("add balance: %w", err)
}

if err = EventsQ(r).Insert(events...); err != nil {
return fmt.Errorf("add open events: %w", err)
}
return nil
})

if err != nil {
Log(r).WithError(err).Error("Failed to add balance with open events")
events := prepareEventsWithRef(did, referredBy, r)
if err = createBalanceWithEvents(did, referredBy, events, r); err != nil {
Log(r).WithError(err).Error("Failed to create balance with events")
ape.RenderErr(w, problems.InternalError())
return
}

// We can't return inserted balance in a single query, because we can't calculate
// rank in transaction: RANK() is a window function allowed on a set of rows,
// while with RETURNING we operate a single one.

// Balance will exist cause of previous logic
// Balance will exist cause of previous logic.
balance, err = getBalanceByDID(did, true, r)
if err != nil {
Log(r).WithError(err).Error("Failed to get balance by DID")
Expand All @@ -107,3 +77,46 @@ func CreateBalance(w http.ResponseWriter, r *http.Request) {

ape.Render(w, newBalanceModel(*balance))
}

func prepareEventsWithRef(did, refBy string, r *http.Request) []data.Event {
events := EventTypes(r).PrepareEvents(did, evtypes.FilterNotOpenable)
if refBy == "" {
return events
}

refType := EventTypes(r).Get(evtypes.TypeBeReferred, evtypes.FilterInactive)
if refType == nil {
Log(r).Debug("Referral event is disabled or expired, skipping it")
return events
}

Log(r).WithFields(map[string]any{"user_did": did, "referred_by": refBy}).
Debug("`Be referred` event will be added for referee user")

return append(events, data.Event{
UserDID: did,
Type: evtypes.TypeBeReferred,
Status: data.EventFulfilled,
})
}

func createBalanceWithEvents(did, refBy string, events []data.Event, r *http.Request) error {
return EventsQ(r).Transaction(func() error {
err := BalancesQ(r).Insert(data.Balance{
DID: did,
ReferralID: referralid.New(did),
ReferredBy: sql.NullString{String: refBy, Valid: refBy != ""},
})

if err != nil {
return fmt.Errorf("add balance: %w", err)
}

Log(r).Debugf("%d events will be added for user_did=%s", len(events), did)
if err = EventsQ(r).Insert(events...); err != nil {
return fmt.Errorf("add open events: %w", err)
}

return nil
})
}
Loading

0 comments on commit 38b030e

Please sign in to comment.