Skip to content

Commit

Permalink
Create/Update support for BillingEntities
Browse files Browse the repository at this point in the history
  • Loading branch information
bastjan committed Apr 13, 2023
1 parent 8fee950 commit ffb18f5
Show file tree
Hide file tree
Showing 16 changed files with 436 additions and 162 deletions.
6 changes: 6 additions & 0 deletions apiserver/billing/odoostorage/odoo/fake/fake.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@ import (
"sync"
"sync/atomic"

"github.com/google/uuid"
"golang.org/x/exp/slices"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
apitypes "k8s.io/apimachinery/pkg/types"

billingv1 "github.com/appuio/control-api/apis/billing/v1"
"github.com/appuio/control-api/apiserver/billing/odoostorage/odoo"
Expand Down Expand Up @@ -45,6 +47,7 @@ func (s *fakeOdooStorage) Create(ctx context.Context, be *billingv1.BillingEntit
id := formatID(s.nextID())

be.Name = id
be.UID = apitypes.UID(uuid.NewString())

s.cleanMetadata(be)

Expand Down Expand Up @@ -104,10 +107,13 @@ func (s *fakeOdooStorage) nextID() uint64 {
func (s *fakeOdooStorage) cleanMetadata(be *billingv1.BillingEntity) {
meta := metav1.ObjectMeta{
Name: be.Name,
// Without UID patch requests fail with a 404 error.
UID: be.UID,
}
if s.metadataSupport {
meta = metav1.ObjectMeta{
Name: be.Name,
UID: be.UID,
ResourceVersion: be.ResourceVersion,
Annotations: be.Annotations,
Labels: be.Labels,
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package model_test

import (
"encoding/json"
"testing"

"github.com/appuio/control-api/apiserver/billing/odoostorage/odoo/odoo8/client/model"
"github.com/stretchr/testify/require"
)

func TestCategoryIDs_MarshalJSON(t *testing.T) {
m, err := json.Marshal(model.CategoryIDs{1, 2, 3})

require.NoError(t, err)
require.Equal(t, `[[6,false,[1,2,3]]]`, string(m))
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package model

import "encoding/json"

type CategoryIDs []int

func (t CategoryIDs) MarshalJSON() ([]byte, error) {
// Values observed on test and prod instances.
// I neither know or care what the fuck they mean.
return json.Marshal([][]any{{6, false, []int(t)}})
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,14 @@ type Nullable[T any] struct {
Valid bool
}

// NewNullable creates a new Nullable[T] with the given value.
func NewNullable[T any](v T) Nullable[T] {
return Nullable[T]{
Value: v,
Valid: true,
}
}

func (t *Nullable[T]) UnmarshalJSON(b []byte) error {
// Odoo returns false (not null) if a field is not set.
if bytes.Equal(b, []byte("false")) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@ import (
"github.com/stretchr/testify/require"
)

func Test_NewNullable(t *testing.T) {
subject := model.NewNullable("test")
require.True(t, subject.Valid)
require.Equal(t, "test", subject.Value)
}

func Test_Nullable(t *testing.T) {
type mt struct {
NullableString model.Nullable[string] `json:"nullable_string"`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,5 +66,10 @@ func (t *OdooCompositeID) UnmarshalJSON(b []byte) error {

// MarshalJSON handles serialization of OdooCompositeID.
func (t OdooCompositeID) MarshalJSON() ([]byte, error) {
return json.Marshal([...]any{t.ID, t.Name})
if !t.Valid {
return []byte("false"), nil
}

// Write path wants just the ID, not the tuple.
return json.Marshal(t.ID)
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,15 @@ import (
)

func TestOdooCompositeIDMarshal(t *testing.T) {
subject := model.OdooCompositeID{ID: 2, Name: "10 Days"}
subject := model.OdooCompositeID{Valid: true, ID: 2, Name: "10 Days"}
marshalled, err := json.Marshal(subject)
require.NoError(t, err)
require.JSONEq(t, fmt.Sprintf(`%d`, subject.ID), string(marshalled))

require.JSONEq(t, string(marshalled), fmt.Sprintf(`[%d,%q]`, subject.ID, subject.Name))

var unmarshalled model.OdooCompositeID
require.NoError(t, json.Unmarshal(marshalled, &unmarshalled))
require.Equal(t, subject.ID, unmarshalled.ID)
subject = model.OdooCompositeID{Valid: false}
marshalled, err = json.Marshal(subject)
require.NoError(t, err)
require.JSONEq(t, "false", string(marshalled))
}

func TestOdooCompositeIDUnmarshal(t *testing.T) {
Expand Down
21 changes: 18 additions & 3 deletions apiserver/billing/odoostorage/odoo/odoo8/client/model/partner.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ import (
"github.com/appuio/control-api/apiserver/billing/odoostorage/odoo/odoo8/client"
)

// PartnerModel is the name of the Odoo model for partners.
const PartnerModel = "res.partner"

// Partner represents a partner ("Customer") record in Odoo
type Partner struct {
// ID is the data record identifier.
Expand All @@ -18,7 +21,7 @@ type Partner struct {
CreationTimestamp client.Date `json:"create_date,omitempty" yaml:"create_date,omitempty"`

// CategoryID is the category of the partner.
CategoryID []int `json:"category_id,omitempty" yaml:"category_id,omitempty"`
CategoryID CategoryIDs `json:"category_id,omitempty" yaml:"category_id,omitempty"`
// Lang is the language of the partner.
Lang Nullable[string] `json:"lang,omitempty" yaml:"lang,omitempty"`
// NotifyEmail is the email notification preference of the partner.
Expand Down Expand Up @@ -48,14 +51,17 @@ type Partner struct {
EmailRaw Nullable[string] `json:"email,omitempty" yaml:"email,omitempty"`
// Phone is the phone number of the partner.
Phone Nullable[string] `json:"phone,omitempty" yaml:"phone,omitempty"`

// Inflight allows detecting half-finished creates.
Inflight Nullable[string] `json:"x_control_api_inflight,omitempty" yaml:"x_control_api_inflight,omitempty"`
}

func (p Partner) Emails() []string {
return splitCommaSeparated(p.EmailRaw.Value)
}

func (p *Partner) SetEmails(emails []string) {
p.EmailRaw = Nullable[string]{Valid: true, Value: strings.Join(emails, ", ")}
p.EmailRaw = NewNullable(strings.Join(emails, ", "))
}

func splitCommaSeparated(s string) []string {
Expand Down Expand Up @@ -116,9 +122,18 @@ func (o Odoo) FetchPartnerByID(ctx context.Context, id int, domainFilters ...cli
func (o Odoo) SearchPartners(ctx context.Context, domainFilters []client.Filter) ([]Partner, error) {
result := &PartnerList{}
err := o.querier.SearchGenericModel(ctx, client.SearchReadModel{
Model: "res.partner",
Model: PartnerModel,
Domain: domainFilters,
Fields: PartnerFields,
}, result)
return result.Items, err
}

func (o Odoo) CreatePartner(ctx context.Context, p Partner) (id int, err error) {
id, err = o.querier.CreateGenericModel(ctx, PartnerModel, p)
return id, err
}

func (o Odoo) UpdateRawPartner(ctx context.Context, ids []int, raw any) error {
return o.querier.UpdateGenericModel(ctx, PartnerModel, ids, raw)
}
81 changes: 0 additions & 81 deletions apiserver/billing/odoostorage/odoo/odoo8/client/model/tax_id.go

This file was deleted.

This file was deleted.

16 changes: 11 additions & 5 deletions apiserver/billing/odoostorage/odoo/odoo8/client/session.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ type QueryExecutor interface {
// CreateGenericModel accepts a payload and executes a query to create the new data record.
CreateGenericModel(ctx context.Context, model string, data any) (int, error)
// UpdateGenericModel accepts a payload and executes a query to update an existing data record.
UpdateGenericModel(ctx context.Context, model string, id int, data any) error
UpdateGenericModel(ctx context.Context, model string, ids []int, data any) error
// DeleteGenericModel accepts a model identifier and data records IDs as payload and executes a query to delete multiple existing data records.
// At least one ID is required.
DeleteGenericModel(ctx context.Context, model string, ids []int) error
Expand Down Expand Up @@ -60,15 +60,21 @@ func (s *Session) CreateGenericModel(ctx context.Context, model string, data any
}

// UpdateGenericModel implements QueryExecutor.
func (s *Session) UpdateGenericModel(ctx context.Context, model string, id int, data any) error {
if id == 0 {
return fmt.Errorf("id cannot be zero: %v", data)
func (s *Session) UpdateGenericModel(ctx context.Context, model string, ids []int, data any) error {
if len(ids) == 0 {
return fmt.Errorf("ids are required")
}
for _, id := range ids {
if id == 0 {
return fmt.Errorf("ids can't be zero: %v", ids)
}
}

payload := WriteModel{
Model: model,
Method: MethodWrite,
Args: []any{
[]int{id},
ids,
data,
},
KWArgs: map[string]any{}, // set to non-null when serializing
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ func TestSession_UpdateGenericModel(t *testing.T) {
require.NoError(t, err)
session := Session{client: &Client{http: http.DefaultClient, parsedURL: u}}
session.client.http.Transport = newDebugTransport()
err = session.UpdateGenericModel(newTestContext(t), "model", 1, "data")
err = session.UpdateGenericModel(newTestContext(t), "model", []int{1}, "data")
require.NoError(t, err)
assert.Equal(t, 1, numRequests)
}
Expand Down
Loading

0 comments on commit ffb18f5

Please sign in to comment.