Skip to content

Commit

Permalink
Add the phone field into the contact
Browse files Browse the repository at this point in the history
  • Loading branch information
Peltoche committed May 23, 2024
1 parent bb96ada commit 568d486
Show file tree
Hide file tree
Showing 22 changed files with 2,370 additions and 14 deletions.
14 changes: 14 additions & 0 deletions assets/public/css/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@

# MDBoostrap upgrade protocol

Before uploading a new version of MDboostrap some manual changes needs to be done.

### Change the flags source file

You need to run the following command:

```bash
sed -i 's#https://mdbootstrap.com/img/svg/flags.png#/assets/images/svg/flags.png#g' ./assets/public/css/mdb.min.css
```

The command above will force the framework to fetch the flags.png assets from our server instead of the mdboostrap.com server.
2 changes: 1 addition & 1 deletion assets/public/css/mdb.min.css

Large diffs are not rendered by default.

Binary file added assets/public/images/svg/flags.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 3 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ require (
github.com/golang-migrate/migrate/v4 v4.17.1
github.com/google/uuid v1.6.0
github.com/neilotoole/slogt v1.1.0
github.com/nyaruka/phonenumbers v1.3.5
github.com/spf13/afero v1.11.0
github.com/spf13/cobra v1.8.0
github.com/spf13/viper v1.18.2
Expand Down Expand Up @@ -52,9 +53,10 @@ require (
go.uber.org/dig v1.17.1 // indirect
go.uber.org/multierr v1.10.0 // indirect
go.uber.org/zap v1.26.0 // indirect
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
golang.org/x/exp v0.0.0-20231214170342-aacd6d4b4611 // indirect
golang.org/x/sys v0.19.0 // indirect
golang.org/x/text v0.14.0 // indirect
google.golang.org/protobuf v1.33.0 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
6 changes: 6 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyua
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/neilotoole/slogt v1.1.0 h1:c7qE92sq+V0yvCuaxph+RQ2jOKL61c4hqS1Bv9W7FZE=
github.com/neilotoole/slogt v1.1.0/go.mod h1:RCrGXkPc/hYybNulqQrMHRtvlQ7F6NktNVLuLwk6V+w=
github.com/nyaruka/phonenumbers v1.3.5 h1:WZLbQn61j2E1OFnvpUTYbK/6hViUgl6tppJ55/E2iQM=
github.com/nyaruka/phonenumbers v1.3.5/go.mod h1:Ut+eFwikULbmCenH6InMKL9csUNLyxHuBLyfkpum11s=
github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4=
github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
Expand Down Expand Up @@ -122,12 +124,16 @@ golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30=
golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=
golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
golang.org/x/exp v0.0.0-20231214170342-aacd6d4b4611 h1:qCEDpW1G+vcj3Y7Fy52pEM1AWm3abj8WimGYejI3SC4=
golang.org/x/exp v0.0.0-20231214170342-aacd6d4b4611/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI=
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
Expand Down
3 changes: 3 additions & 0 deletions internal/migrations/0002_init_phones_table.down.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
DROP TABLE IF EXISTS phones;

DROP INDEX IF EXISTS idx_phones_id;
15 changes: 15 additions & 0 deletions internal/migrations/0002_init_phones_table.up.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
CREATE TABLE IF NOT EXISTS phones (
"id" TEXT NOT NULL,
"type" TEXT NOT NULL,
"iso2_region_code" TEXT NOT NULL,
"international_formatted" TEXT NOT NULL,
"national_formatted" TEXT NOT NULL,
"normalized" TEXT NOT NULL,
"contact_id" TEXT NOT NULL,
"created_at" TEXT NOT NULL,
FOREIGN KEY(contact_id) REFERENCES contacts(id) ON UPDATE RESTRICT ON DELETE CASCADE
) STRICT;

CREATE UNIQUE INDEX IF NOT EXISTS idx_phones_id ON phones(id);
CREATE INDEX IF NOT EXISTS idx_phones_international_id ON phones(international_formatted);
CREATE INDEX IF NOT EXISTS idx_phones_national_id ON phones(national_formatted);
2 changes: 2 additions & 0 deletions internal/server/start.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"github.com/Peltoche/gnocchi/assets"
"github.com/Peltoche/gnocchi/internal/migrations"
"github.com/Peltoche/gnocchi/internal/service/contacts"
"github.com/Peltoche/gnocchi/internal/service/phonenumbers"
"github.com/Peltoche/gnocchi/internal/service/utilities"
"github.com/Peltoche/gnocchi/internal/tools"
"github.com/Peltoche/gnocchi/internal/tools/logger"
Expand Down Expand Up @@ -79,6 +80,7 @@ func start(ctx context.Context, cfg Config, invoke fx.Option) *fx.App {

// Services
fx.Annotate(contacts.Init, fx.As(new(contacts.Service))),
fx.Annotate(phonenumbers.Init, fx.As(new(phonenumbers.Service))),

// HTTP handlers
AsRoute(assets.NewHTTPHandler),
Expand Down
14 changes: 14 additions & 0 deletions internal/service/contacts/model_helper.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
package contacts

import (
"context"
"testing"
"time"

"github.com/Peltoche/gnocchi/internal/tools/sqlstorage"
"github.com/Peltoche/gnocchi/internal/tools/uuid"
"github.com/brianvoe/gofakeit/v7"
"github.com/stretchr/testify/require"
)

type FakeContactBuilder struct {
Expand Down Expand Up @@ -38,3 +41,14 @@ func NewFakeContact(t testing.TB) *FakeContactBuilder {
func (f *FakeContactBuilder) Build() *Contact {
return f.contact
}

func (f *FakeContactBuilder) BuildAndStore(ctx context.Context, db sqlstorage.Querier) *Contact {
f.t.Helper()

storage := newSqlStorage(db)

err := storage.Save(ctx, f.contact)
require.NoError(f.t, err)

return f.contact
}
24 changes: 24 additions & 0 deletions internal/service/phonenumbers/init.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package phonenumbers

import (
"context"

"github.com/Peltoche/gnocchi/internal/service/contacts"
"github.com/Peltoche/gnocchi/internal/tools"
"github.com/Peltoche/gnocchi/internal/tools/sqlstorage"
)

//go:generate mockery --name Service
type Service interface {
Create(ctx context.Context, cmd *CreateCmd) (*Phone, error)
GetAllForContact(ctx context.Context, contact *contacts.Contact, cmd *sqlstorage.PaginateCmd) ([]Phone, error)
}

func Init(
tools tools.Tools,
db sqlstorage.Querier,
) Service {
store := newSqlStorage(db)

return newService(tools, store)
}
34 changes: 34 additions & 0 deletions internal/service/phonenumbers/model.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package phonenumbers

import (
"time"

"github.com/Peltoche/gnocchi/internal/service/contacts"
"github.com/Peltoche/gnocchi/internal/tools/uuid"
)

type Phone struct {
createdAt time.Time
id uuid.UUID
phoneType string
internationalFormatted string
nationalFormatted string
normalized string
contactID uuid.UUID
iso2RegionCode string
}

func (p Phone) ID() uuid.UUID { return p.id }
func (p Phone) Type() string { return p.phoneType }
func (p Phone) InternationalFormatted() string { return p.internationalFormatted }
func (p Phone) NationalFormatted() string { return p.nationalFormatted }
func (p Phone) ISO2RegionCode() string { return p.iso2RegionCode }
func (p Phone) ContactID() uuid.UUID { return p.contactID }
func (p Phone) CreatedAt() time.Time { return p.createdAt }

type CreateCmd struct {
Contact *contacts.Contact
Type string
Region string
Input string
}
52 changes: 52 additions & 0 deletions internal/service/phonenumbers/model_helper.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package phonenumbers

import (
"testing"
"time"

"github.com/Peltoche/gnocchi/internal/service/contacts"
"github.com/Peltoche/gnocchi/internal/tools/uuid"
"github.com/brianvoe/gofakeit/v7"
"github.com/nyaruka/phonenumbers"
"github.com/stretchr/testify/require"
)

type FakePhoneBuilder struct {
t testing.TB
phone *Phone
}

func NewFakePhone(t testing.TB) *FakePhoneBuilder {
t.Helper()

uuidProvider := uuid.NewProvider()
createdAt := gofakeit.DateRange(time.Now().Add(-time.Hour*1000), time.Now())

iso2RegionCode := "US"

num, err := phonenumbers.Parse(gofakeit.Phone(), iso2RegionCode)
require.NoError(t, err)

return &FakePhoneBuilder{
t: t,
phone: &Phone{
id: uuidProvider.New(),
phoneType: "Home",
iso2RegionCode: iso2RegionCode,
internationalFormatted: phonenumbers.Format(num, phonenumbers.INTERNATIONAL),
nationalFormatted: phonenumbers.Format(num, phonenumbers.NATIONAL),
contactID: uuidProvider.New(),
createdAt: createdAt,
},
}
}

func (f *FakePhoneBuilder) WithContact(contact *contacts.Contact) *FakePhoneBuilder {
f.phone.contactID = contact.ID()

return f
}

func (f *FakePhoneBuilder) Build() *Phone {
return f.phone
}
19 changes: 19 additions & 0 deletions internal/service/phonenumbers/model_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package phonenumbers

import (
"testing"

"github.com/stretchr/testify/assert"
)

func Test_Phone_model_getters(t *testing.T) {
phone := NewFakePhone(t).Build()

assert.Equal(t, phone.id, phone.ID())
assert.Equal(t, phone.phoneType, phone.Type())
assert.Equal(t, phone.internationalFormatted, phone.InternationalFormatted())
assert.Equal(t, phone.nationalFormatted, phone.NationalFormatted())
assert.Equal(t, phone.iso2RegionCode, phone.ISO2RegionCode())
assert.Equal(t, phone.contactID, phone.ContactID())
assert.Equal(t, phone.createdAt, phone.CreatedAt())
}
82 changes: 82 additions & 0 deletions internal/service/phonenumbers/service.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package phonenumbers

import (
"context"
"errors"
"fmt"
"strings"

"github.com/Peltoche/gnocchi/internal/service/contacts"
"github.com/Peltoche/gnocchi/internal/tools"
"github.com/Peltoche/gnocchi/internal/tools/clock"
"github.com/Peltoche/gnocchi/internal/tools/errs"
"github.com/Peltoche/gnocchi/internal/tools/sqlstorage"
"github.com/Peltoche/gnocchi/internal/tools/uuid"
"github.com/nyaruka/phonenumbers"
)

//go:generate mockery --name storage
type storage interface {
Save(ctx context.Context, p *Phone) error
GetAllForContact(ctx context.Context, contact *contacts.Contact, cmd *sqlstorage.PaginateCmd) ([]Phone, error)
}

type service struct {
storage storage
uuid uuid.Service
clock clock.Clock
}

// newService create a new user service.
func newService(tools tools.Tools, storage storage) *service {
return &service{
storage: storage,
uuid: tools.UUID(),
clock: tools.Clock(),
}
}

func (s *service) Create(ctx context.Context, cmd *CreateCmd) (*Phone, error) {
number, err := phonenumbers.Parse(cmd.Input, strings.ToUpper(cmd.Region))
if err != nil {
return nil, errs.Validation(fmt.Errorf("invalid phone number %q: %w", cmd.Input, err))
}

internationalFormated := phonenumbers.Format(number, phonenumbers.INTERNATIONAL)

phoneType := strings.TrimSpace(cmd.Type)
if len(phoneType) == 0 {
return nil, errs.Validation(errors.New("invalid/missing phone type"))
}

phone := Phone{
createdAt: s.clock.Now(),
id: s.uuid.New(),
phoneType: phoneType,
internationalFormatted: internationalFormated,
nationalFormatted: phonenumbers.Format(number, phonenumbers.NATIONAL),
normalized: strings.NewReplacer(" ", "", "+", "").Replace(internationalFormated),
contactID: cmd.Contact.ID(),
iso2RegionCode: strings.ToUpper(cmd.Region),
}

err = s.storage.Save(ctx, &phone)
if err != nil {
return nil, fmt.Errorf("failed to save into storage: %w", err)
}

return &phone, nil
}

func (s *service) GetAllForContact(ctx context.Context, contact *contacts.Contact, cmd *sqlstorage.PaginateCmd) ([]Phone, error) {
res, err := s.storage.GetAllForContact(ctx, contact, cmd)
if errors.Is(err, errNotFound) {
return nil, errs.NotFound(err)
}

if err != nil {
return nil, errs.Internal(err)
}

return res, nil
}
Loading

0 comments on commit 568d486

Please sign in to comment.