add sale order

This commit is contained in:
nochill 2023-03-22 12:06:09 +07:00
parent 2de83644b8
commit 5604f34e97
30 changed files with 592 additions and 67 deletions

View File

@ -21,27 +21,27 @@ func authMiddleware(tokenMaker token.Maker) gin.HandlerFunc {
authorizationHeader := ctx.GetHeader(authorizationHeaderKey)
if len(authorizationHeader) == 0 {
err := errors.New("authorization header is not provided")
ctx.AbortWithStatusJSON(http.StatusUnauthorized, errorResponse(err))
ctx.AbortWithStatusJSON(http.StatusUnauthorized, errorResponse(err, ""))
return
}
fields := strings.Fields(authorizationHeader)
if len(fields) < 2 {
err := errors.New("Invalid authorization header format")
ctx.AbortWithStatusJSON(http.StatusUnauthorized, errorResponse(err))
ctx.AbortWithStatusJSON(http.StatusUnauthorized, errorResponse(err, ""))
return
}
authorizationType := strings.ToLower(fields[0])
if authorizationType != authorizationTypeBearer {
err := fmt.Errorf("Authorization only accept bearer type")
ctx.AbortWithStatusJSON(http.StatusUnauthorized, errorResponse(err))
ctx.AbortWithStatusJSON(http.StatusUnauthorized, errorResponse(err, ""))
}
accessToken := fields[1]
payload, err := tokenMaker.VerifyToken(accessToken)
if err != nil {
ctx.AbortWithStatusJSON(http.StatusUnauthorized, errorResponse(err))
ctx.AbortWithStatusJSON(http.StatusUnauthorized, errorResponse(err, ""))
return
}

View File

@ -24,7 +24,7 @@ func (server *Server) createProduct(ctx *gin.Context) {
var req createProductRequest
if err := ctx.ShouldBindJSON(&req); err != nil {
ctx.JSON(http.StatusBadRequest, errorResponse(err))
ctx.JSON(http.StatusBadRequest, errorResponse(err, ""))
return
}
@ -40,7 +40,7 @@ func (server *Server) createProduct(ctx *gin.Context) {
product, err := server.store.CreateProduct(ctx, arg)
if err != nil {
ctx.JSON(http.StatusInternalServerError, errorResponse(err))
ctx.JSON(http.StatusInternalServerError, errorResponse(err, ""))
return
}
@ -54,24 +54,24 @@ type getProductRequest struct {
func (server *Server) getProduct(ctx *gin.Context) {
var req getProductRequest
if err := ctx.ShouldBindUri(&req); err != nil {
ctx.JSON(http.StatusBadRequest, errorResponse(err))
ctx.JSON(http.StatusBadRequest, errorResponse(err, ""))
return
}
product, err := server.store.GetProduct(ctx, uuid.MustParse(req.ID))
if err != nil {
if err == sql.ErrNoRows {
ctx.JSON(http.StatusNotFound, errorResponse(err))
ctx.JSON(http.StatusNotFound, errorResponse(err, ""))
return
}
ctx.JSON(http.StatusInternalServerError, errorResponse(err))
ctx.JSON(http.StatusInternalServerError, errorResponse(err, ""))
return
}
authPayload := ctx.MustGet(authorizationPayloadKey).(*token.Payload)
if product.MerchantID != authPayload.MerchantID {
err := errors.New("Product doesn't belong to the user")
ctx.JSON(http.StatusUnauthorized, errorResponse(err))
ctx.JSON(http.StatusUnauthorized, errorResponse(err, ""))
return
}
@ -86,7 +86,7 @@ type listProductsRequest struct {
func (server *Server) listProducts(ctx *gin.Context) {
var req listProductsRequest
if err := ctx.ShouldBindQuery(&req); err != nil {
ctx.JSON(http.StatusBadRequest, errorResponse(err))
ctx.JSON(http.StatusBadRequest, errorResponse(err, ""))
return
}
@ -99,7 +99,7 @@ func (server *Server) listProducts(ctx *gin.Context) {
products, err := server.store.ListProducts(ctx, arg)
if err != nil {
ctx.JSON(http.StatusInternalServerError, errorResponse(err))
ctx.JSON(http.StatusInternalServerError, errorResponse(err, ""))
return
}
@ -116,7 +116,7 @@ type updateProductRequest struct {
func (server *Server) updateProduct(ctx *gin.Context) {
var req updateProductRequest
if err := ctx.ShouldBindJSON(&req); err != nil {
ctx.JSON(http.StatusBadRequest, errorResponse(err))
ctx.JSON(http.StatusBadRequest, errorResponse(err, ""))
return
}
@ -131,10 +131,10 @@ func (server *Server) updateProduct(ctx *gin.Context) {
product, err := server.store.UpdateProduct(ctx, arg)
if err != nil {
if err == sql.ErrNoRows {
ctx.JSON(http.StatusNotFound, errorResponse(err))
ctx.JSON(http.StatusNotFound, errorResponse(err, ""))
return
}
ctx.JSON(http.StatusInternalServerError, errorResponse(err))
ctx.JSON(http.StatusInternalServerError, errorResponse(err, ""))
return
}

View File

@ -19,7 +19,7 @@ func (server *Server) createProductCategory(ctx *gin.Context) {
var req createProductCategoryRequest
if err := ctx.ShouldBindJSON(&req); err != nil {
ctx.JSON(http.StatusBadRequest, errorResponse(err))
ctx.JSON(http.StatusBadRequest, errorResponse(err, ""))
return
}
@ -31,7 +31,7 @@ func (server *Server) createProductCategory(ctx *gin.Context) {
ProductCategory, err := server.store.CreateProductCategory(ctx, arg)
if err != nil {
ctx.JSON(http.StatusInternalServerError, errorResponse(err))
ctx.JSON(http.StatusInternalServerError, errorResponse(err, ""))
return
}
@ -46,7 +46,7 @@ type listProductCategoriesRequest struct {
func (server *Server) listProductCategories(ctx *gin.Context) {
var req listProductCategoriesRequest
if err := ctx.ShouldBindQuery(&req); err != nil {
ctx.JSON(http.StatusBadRequest, errorResponse(err))
ctx.JSON(http.StatusBadRequest, errorResponse(err, ""))
return
}
@ -59,7 +59,7 @@ func (server *Server) listProductCategories(ctx *gin.Context) {
ProductCategories, err := server.store.ListProductCategoriesByMerchantID(ctx, arg)
if err != nil {
ctx.JSON(http.StatusInternalServerError, errorResponse(err))
ctx.JSON(http.StatusInternalServerError, errorResponse(err, ""))
return
}
@ -74,7 +74,7 @@ type updateProductCategoryRequest struct {
func (server *Server) updateProductCategory(ctx *gin.Context) {
var req updateProductCategoryRequest
if err := ctx.ShouldBindJSON(&req); err != nil {
ctx.JSON(http.StatusBadRequest, errorResponse(err))
ctx.JSON(http.StatusBadRequest, errorResponse(err, ""))
return
}
@ -86,10 +86,10 @@ func (server *Server) updateProductCategory(ctx *gin.Context) {
ProductCategory, err := server.store.UpdateProductCategory(ctx, arg)
if err != nil {
if err == sql.ErrNoRows {
ctx.JSON(http.StatusNotFound, errorResponse(err))
ctx.JSON(http.StatusNotFound, errorResponse(err, ""))
return
}
ctx.JSON(http.StatusInternalServerError, errorResponse(err))
ctx.JSON(http.StatusInternalServerError, errorResponse(err, ""))
return
}

View File

@ -35,11 +35,11 @@ func TestCreateProductCategory(t *testing.T) {
"merchant_id": productCategory.MerchantID,
},
setupAuth: func(t *testing.T, request *http.Request, tokenMaker token.Maker) {
addAuthorization(t, request, tokenMaker, authorizationTypeBearer, util.RandomEmail(), "a848090f-0409-4386-9caa-929ae6874dbb", time.Minute)
addAuthorization(t, request, tokenMaker, authorizationTypeBearer, util.RandomEmail(), "54b8a2d9-16be-4239-8828-5daa317028dc", time.Minute)
},
buildStubs: func(store *mockdb.MockStore) {
arg := db.CreateProductCategoryParams{
MerchantID: uuid.MustParse("a848090f-0409-4386-9caa-929ae6874dbb"),
MerchantID: uuid.MustParse("54b8a2d9-16be-4239-8828-5daa317028dc"),
Name: productCategory.Name,
}
store.EXPECT().
@ -83,7 +83,7 @@ func TestCreateProductCategory(t *testing.T) {
func createRandomProductCategory() db.ProductCategory {
return db.ProductCategory{
ID: uuid.New(),
MerchantID: uuid.MustParse("a848090f-0409-4386-9caa-929ae6874dbb"),
MerchantID: uuid.MustParse("54b8a2d9-16be-4239-8828-5daa317028dc"),
Name: util.RandomString(5),
}
}

View File

@ -21,7 +21,7 @@ import (
"github.com/stretchr/testify/require"
)
var MERCHANTID = "a848090f-0409-4386-9caa-929ae6874dbb"
var MERCHANTID = "54b8a2d9-16be-4239-8828-5daa317028dc"
func TestGetProductApi(t *testing.T) {
product := randomProduct(MERCHANTID)

View File

@ -34,7 +34,7 @@ func (server Server) createPurchase(ctx *gin.Context) {
}
if err := ctx.ShouldBindJSON(&req); err != nil {
ctx.JSON(http.StatusBadRequest, errorResponse(err))
ctx.JSON(http.StatusBadRequest, errorResponse(err, ""))
return
}
@ -52,7 +52,7 @@ func (server Server) createPurchase(ctx *gin.Context) {
result, err := server.store.PurchaseOrderTx(ctx, arg)
if err != nil {
ctx.JSON(http.StatusInternalServerError, errorResponse(err))
ctx.JSON(http.StatusInternalServerError, errorResponse(err, ""))
return
}

78
api/sale_order.go Normal file
View File

@ -0,0 +1,78 @@
package api
import (
"database/sql"
"net/http"
db "git.nochill.in/nochill/naice_pos/db/sqlc"
"git.nochill.in/nochill/naice_pos/util"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"github.com/lib/pq"
)
type createSaleOrderRequest struct {
MerchantID uuid.UUID `json:"merchant_id" binding:"required"`
MerchantIDX int64 `json:"merchant_index" binding:"required"`
CustomerID string `json:"customer_id"`
CreatedBy uuid.UUID `json:"created_by" binding:"required"`
Code string `json:"code"`
IsPaid bool `json:"is_paid" binding:"required"`
Total float64 `json:"total" binding:"required"`
PaidNominal float64 `json:"paid_nominal" binding:"required"`
Change float64 `json:"change"`
Note string `json:"note"`
IsKeep bool `json:"is_keep"`
Products []db.SaleOrderProduct `json:"products" binding:"required"`
}
func (server Server) createSaleOrder(ctx *gin.Context) {
var req createSaleOrderRequest
code := util.RandomTransactionCode("S", req.MerchantIDX)
customerID := uuid.NullUUID{Valid: false, UUID: uuid.Nil}
if err := ctx.ShouldBindJSON(&req); err != nil {
ctx.JSON(http.StatusBadRequest, errorResponse(err, ""))
return
}
if len(req.CustomerID) > 0 {
customerID = uuid.NullUUID{Valid: true, UUID: uuid.MustParse(req.CustomerID)}
}
if len(req.Code) > 0 {
code = req.Code
}
arg := db.SaleOrderTxParams{
MerchantID: req.MerchantID,
CreatedBy: req.CreatedBy,
CustomerID: customerID,
Code: code,
IsPaid: req.IsPaid,
Total: req.Total,
PaidNominal: req.PaidNominal,
Note: sql.NullString{Valid: len(req.Note) > 0, String: req.Note},
IsKeep: req.IsKeep,
Change: req.Change,
Products: req.Products,
}
result, err := server.store.SaleOrderTx(ctx, arg)
if err != nil {
if pqErr, ok := err.(*pq.Error); ok {
switch pqErr.Code.Name() {
case "check_violation":
ctx.JSON(http.StatusConflict, errorResponse(err, "Stok tidak bisa kurang dari 0"))
return
}
}
ctx.JSON(http.StatusInternalServerError, errorResponse(err, ""))
return
}
ctx.JSON(http.StatusOK, result)
}

View File

@ -42,6 +42,7 @@ func (server *Server) getRoutes() {
apiRoutes.POST("/products", server.createProduct)
apiRoutes.PATCH("/products", server.updateProduct)
apiRoutes.GET("/products", server.listProducts)
apiRoutes.GET("/product/:id", server.getProduct)
apiRoutes.POST("/product/category", server.createProductCategory)
@ -53,6 +54,8 @@ func (server *Server) getRoutes() {
apiRoutes.POST("/purchase-products", server.createPurchase)
apiRoutes.POST("/sale-order", server.createSaleOrder)
server.router = router
}

View File

@ -26,7 +26,7 @@ func (server *Server) createSupplier(ctx *gin.Context) {
var supDetail pqtype.NullRawMessage
if err := ctx.ShouldBindJSON(&req); err != nil {
ctx.JSON(http.StatusBadRequest, errorResponse(err))
ctx.JSON(http.StatusBadRequest, errorResponse(err, ""))
return
}
@ -44,7 +44,7 @@ func (server *Server) createSupplier(ctx *gin.Context) {
supplier, err := server.store.CreateSuppliers(ctx, arg)
if err != nil {
ctx.JSON(http.StatusInternalServerError, errorResponse(err))
ctx.JSON(http.StatusInternalServerError, errorResponse(err, ""))
return
}

View File

@ -21,63 +21,63 @@ type renewAccessResponse struct {
func (server *Server) renewAccessToken(ctx *gin.Context) {
var req renewAccessRequest
if err := ctx.ShouldBindJSON(&req); err != nil {
ctx.JSON(http.StatusBadRequest, errorResponse(err))
ctx.JSON(http.StatusBadRequest, errorResponse(err, ""))
return
}
refreshPayload, err := server.tokenMaker.VerifyToken(req.RefreshToken)
if err != nil {
ctx.JSON(http.StatusUnauthorized, errorResponse(err))
ctx.JSON(http.StatusUnauthorized, errorResponse(err, ""))
return
}
session, err := server.store.GetSession(ctx, refreshPayload.ID)
if err != nil {
if err == sql.ErrNoRows {
ctx.JSON(http.StatusNotFound, errorResponse(err))
ctx.JSON(http.StatusNotFound, errorResponse(err, ""))
return
}
ctx.JSON(http.StatusInternalServerError, errorResponse(err))
ctx.JSON(http.StatusInternalServerError, errorResponse(err, ""))
return
}
if session.IsBlocked {
err := fmt.Errorf("blocked session")
ctx.JSON(http.StatusUnauthorized, errorResponse(err))
ctx.JSON(http.StatusUnauthorized, errorResponse(err, ""))
return
}
if session.Email != refreshPayload.Email {
err := fmt.Errorf("incorrect session user")
ctx.JSON(http.StatusUnauthorized, errorResponse(err))
ctx.JSON(http.StatusUnauthorized, errorResponse(err, ""))
return
}
if session.RefreshToken != req.RefreshToken {
err := fmt.Errorf("mismatched session token")
ctx.JSON(http.StatusUnauthorized, errorResponse(err))
ctx.JSON(http.StatusUnauthorized, errorResponse(err, ""))
return
}
if time.Now().After(refreshPayload.ExpiredAt) {
err := fmt.Errorf("Exprired session")
ctx.JSON(http.StatusUnauthorized, errorResponse(err))
ctx.JSON(http.StatusUnauthorized, errorResponse(err, ""))
return
}
user, err := server.store.GetUserByEmail(ctx, refreshPayload.Email)
if err != nil {
if err == sql.ErrNoRows {
ctx.JSON(http.StatusNotFound, errorResponse(err))
ctx.JSON(http.StatusNotFound, errorResponse(err, ""))
return
}
ctx.JSON(http.StatusInternalServerError, errorResponse(err))
ctx.JSON(http.StatusInternalServerError, errorResponse(err, ""))
return
}
merchant, err := server.store.GetMerchantByUserId(ctx, user.ID)
if err != nil {
ctx.JSON(http.StatusInternalServerError, errorResponse(err))
ctx.JSON(http.StatusInternalServerError, errorResponse(err, ""))
return
}
@ -88,7 +88,7 @@ func (server *Server) renewAccessToken(ctx *gin.Context) {
)
if err != nil {
ctx.JSON(http.StatusInternalServerError, errorResponse(err))
ctx.JSON(http.StatusInternalServerError, errorResponse(err, ""))
return
}

View File

@ -50,13 +50,13 @@ func newUserMerchantResponse(user db.GetUserByEmailRow) userMerchantResponse {
func (server *Server) createUserMerchant(ctx *gin.Context) {
var req createUserMerchantRequest
if err := ctx.ShouldBindJSON(&req); err != nil {
ctx.JSON(http.StatusBadRequest, errorResponse(err))
ctx.JSON(http.StatusBadRequest, errorResponse(err, ""))
return
}
hashedPassword, err := util.HashPassword(req.Password)
if err != nil {
ctx.JSON(http.StatusInternalServerError, errorResponse(err))
ctx.JSON(http.StatusInternalServerError, errorResponse(err, ""))
return
}
@ -72,11 +72,11 @@ func (server *Server) createUserMerchant(ctx *gin.Context) {
if pqErr, ok := err.(*pq.Error); ok {
switch pqErr.Code.Name() {
case "foreign_key_violation", "unique_violation":
ctx.JSON(http.StatusConflict, errorResponse(err))
ctx.JSON(http.StatusConflict, errorResponse(err, ""))
return
}
}
ctx.JSON(http.StatusInternalServerError, errorResponse(err))
ctx.JSON(http.StatusInternalServerError, errorResponse(err, ""))
return
}
@ -117,23 +117,23 @@ type userLoginResponse struct {
func (server *Server) loginUser(ctx *gin.Context) {
var req userLoginRequest
if err := ctx.ShouldBindJSON(&req); err != nil {
ctx.JSON(http.StatusBadRequest, errorResponse(err))
ctx.JSON(http.StatusBadRequest, errorResponse(err, ""))
return
}
user, err := server.store.GetUserByEmail(ctx, req.Email)
if err != nil {
if err == sql.ErrNoRows {
ctx.JSON(http.StatusNotFound, errorResponse(err))
ctx.JSON(http.StatusNotFound, errorResponse(err, ""))
return
}
ctx.JSON(http.StatusInternalServerError, errorResponse(err))
ctx.JSON(http.StatusInternalServerError, errorResponse(err, ""))
return
}
err = util.CheckPassword(req.Password, user.Password)
if err != nil {
ctx.JSON(http.StatusUnauthorized, errorResponse(err))
ctx.JSON(http.StatusUnauthorized, errorResponse(err, ""))
return
}
@ -144,7 +144,7 @@ func (server *Server) loginUser(ctx *gin.Context) {
)
if err != nil {
ctx.JSON(http.StatusInternalServerError, errorResponse(err))
ctx.JSON(http.StatusInternalServerError, errorResponse(err, ""))
return
}
@ -165,7 +165,7 @@ func (server *Server) loginUser(ctx *gin.Context) {
})
if err != nil {
ctx.JSON(http.StatusInternalServerError, errorResponse(err))
ctx.JSON(http.StatusInternalServerError, errorResponse(err, ""))
return
}

View File

@ -44,7 +44,7 @@ CREATE TABLE products (
"name" varchar not null,
"selling_price" double precision default(0::double precision) NOT NULL,
"purchase_price" double precision default(0:: double precision) NOT NULL,
"stock" double precision default(0::double precision) NOT NULL,
"stock" double precision default(0::double precision) NOT NULL CHECK("stock" >= 0),
"created_at" timestamp default(now()),
"updated_at" timestamp default(now())
);
@ -94,6 +94,7 @@ CREATE TABLE sale_order (
"created_at" timestamp default(now()),
"updated_at" timestamp default(now())
);
CREATE TABLE sale_order_detail (
"id" uuid default gen_random_uuid() primary key not null,
"index_id" bigserial not null,

View File

@ -0,0 +1 @@
ALTER TABLE sale_order DROP COLUMN IF EXISTS is_keep;

View File

@ -0,0 +1 @@
ALTER TABLE sale_order ADD COLUMN is_keep boolean not null default false;

View File

@ -0,0 +1,3 @@
ALTER TABLE sale_order DROP COLUMN change;
ALTER TABLE sale_order ALTER COLUMN is_paid DROP NOT null;
ALTER TABLE sale_order ALTER COLUMN code DROP NOT null;

View File

@ -0,0 +1,3 @@
ALTER TABLE sale_order ADD COLUMN change double precision default(0::double precision) NOT NULL;
ALTER TABLE sale_order ALTER COLUMN is_paid SET not null;
ALTER TABLE sale_order ALTER COLUMN code SET not null;

View File

@ -127,10 +127,10 @@ func (mr *MockStoreMockRecorder) CreatePurchaseOrderDetail(arg0, arg1 interface{
}
// CreateSaleOrder mocks base method.
func (m *MockStore) CreateSaleOrder(arg0 context.Context, arg1 db.CreateSaleOrderParams) (db.PurchaseOrder, error) {
func (m *MockStore) CreateSaleOrder(arg0 context.Context, arg1 db.CreateSaleOrderParams) (db.SaleOrder, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CreateSaleOrder", arg0, arg1)
ret0, _ := ret[0].(db.PurchaseOrder)
ret0, _ := ret[0].(db.SaleOrder)
ret1, _ := ret[1].(error)
return ret0, ret1
}
@ -141,6 +141,21 @@ func (mr *MockStoreMockRecorder) CreateSaleOrder(arg0, arg1 interface{}) *gomock
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateSaleOrder", reflect.TypeOf((*MockStore)(nil).CreateSaleOrder), arg0, arg1)
}
// CreateSaleOrderDetail mocks base method.
func (m *MockStore) CreateSaleOrderDetail(arg0 context.Context, arg1 db.CreateSaleOrderDetailParams) (db.SaleOrderDetail, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CreateSaleOrderDetail", arg0, arg1)
ret0, _ := ret[0].(db.SaleOrderDetail)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// CreateSaleOrderDetail indicates an expected call of CreateSaleOrderDetail.
func (mr *MockStoreMockRecorder) CreateSaleOrderDetail(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateSaleOrderDetail", reflect.TypeOf((*MockStore)(nil).CreateSaleOrderDetail), arg0, arg1)
}
// CreateSession mocks base method.
func (m *MockStore) CreateSession(arg0 context.Context, arg1 db.CreateSessionParams) (db.UserSession, error) {
m.ctrl.T.Helper()
@ -497,6 +512,21 @@ func (mr *MockStoreMockRecorder) PurchaseOrderTx(arg0, arg1 interface{}) *gomock
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PurchaseOrderTx", reflect.TypeOf((*MockStore)(nil).PurchaseOrderTx), arg0, arg1)
}
// SaleOrderTx mocks base method.
func (m *MockStore) SaleOrderTx(arg0 context.Context, arg1 db.SaleOrderTxParams) (db.SaleOrderTxResult, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SaleOrderTx", arg0, arg1)
ret0, _ := ret[0].(db.SaleOrderTxResult)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// SaleOrderTx indicates an expected call of SaleOrderTx.
func (mr *MockStoreMockRecorder) SaleOrderTx(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaleOrderTx", reflect.TypeOf((*MockStore)(nil).SaleOrderTx), arg0, arg1)
}
// SuppliersList mocks base method.
func (m *MockStore) SuppliersList(arg0 context.Context, arg1 db.SuppliersListParams) ([]db.Supplier, error) {
m.ctrl.T.Helper()

16
db/query/sale_order.sql Normal file
View File

@ -0,0 +1,16 @@
-- name: CreateSaleOrder :one
INSERT INTO sale_order (
merchant_id,
customer_id,
code,
created_by,
is_paid,
total,
paid_nominal,
note,
change,
is_keep
) VALUES (
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10
)
RETURNING *;

View File

@ -0,0 +1,13 @@
-- name: CreateSaleOrderDetail :one
INSERT INTO sale_order_detail (
sale_order_id,
product_id,
product_name,
quantity,
sub_total,
product_price,
profit
) VALUES(
$1, $2, $3, $4, $5, $6, $7
)
RETURNING *;

34
db/sqlc/customer_test.go Normal file
View File

@ -0,0 +1,34 @@
package db
import (
"context"
"testing"
"git.nochill.in/nochill/naice_pos/util"
"github.com/google/uuid"
"github.com/stretchr/testify/require"
)
func createRandomCustomer(t *testing.T) (Customer, CreateCustomersParams) {
arg := CreateCustomersParams{
MerchantID: uuid.MustParse("54b8a2d9-16be-4239-8828-5daa317028dc"),
Name: util.RandomString(10),
}
customer, err := testQueries.CreateCustomers(context.Background(), arg)
require.NoError(t, err)
return customer, arg
}
func TestCreateCustomer(t *testing.T) {
supplier, arg := createRandomCustomer(t)
require.Equal(t, arg.Name, supplier.Name)
require.Equal(t, arg.MerchantID, supplier.MerchantID)
require.NotZero(t, supplier.ID)
require.NotZero(t, supplier.CreatedAt)
require.NotZero(t, supplier.UpdatedAt)
}

View File

@ -128,16 +128,18 @@ type PurchaseOrderDetail struct {
type SaleOrder struct {
ID uuid.UUID `json:"id"`
IndexID int64 `json:"index_id"`
Code sql.NullString `json:"code"`
Code string `json:"code"`
CreatedBy uuid.UUID `json:"created_by"`
MerchantID uuid.UUID `json:"merchant_id"`
CustomerID uuid.NullUUID `json:"customer_id"`
IsPaid sql.NullBool `json:"is_paid"`
IsPaid bool `json:"is_paid"`
Total float64 `json:"total"`
PaidNominal float64 `json:"paid_nominal"`
Note sql.NullString `json:"note"`
CreatedAt sql.NullTime `json:"created_at"`
UpdatedAt sql.NullTime `json:"updated_at"`
IsKeep bool `json:"is_keep"`
Change float64 `json:"change"`
}
type SaleOrderDetail struct {

View File

@ -9,7 +9,7 @@ import (
"github.com/stretchr/testify/require"
)
var merchantID = uuid.MustParse("04a1b0a7-69b4-41da-a053-2f6b95c93195")
var merchantID = uuid.MustParse("54b8a2d9-16be-4239-8828-5daa317028dc")
func createRandomProductCategory(t *testing.T) ProductCategory {
arg := CreateProductCategoryParams{

View File

@ -19,7 +19,7 @@ func createRandomProduct(t *testing.T) (Product, CreateProductParams) {
productCategory := createRandomProductCategory(t)
arg := CreateProductParams{
MerchantID: uuid.MustParse("04a1b0a7-69b4-41da-a053-2f6b95c93195"),
MerchantID: uuid.MustParse("54b8a2d9-16be-4239-8828-5daa317028dc"),
Name: util.RandomString(10),
SellingPrice: sellingPrice,
PurchasePrice: purchasePrice,

View File

@ -17,7 +17,8 @@ type Querier interface {
CreateProductCategory(ctx context.Context, arg CreateProductCategoryParams) (ProductCategory, error)
CreatePurchaseOrder(ctx context.Context, arg CreatePurchaseOrderParams) (PurchaseOrder, error)
CreatePurchaseOrderDetail(ctx context.Context, arg CreatePurchaseOrderDetailParams) (PurchaseOrderDetail, error)
CreateSaleOrder(ctx context.Context, arg CreateSaleOrderParams) (PurchaseOrder, error)
CreateSaleOrder(ctx context.Context, arg CreateSaleOrderParams) (SaleOrder, error)
CreateSaleOrderDetail(ctx context.Context, arg CreateSaleOrderDetailParams) (SaleOrderDetail, error)
CreateSession(ctx context.Context, arg CreateSessionParams) (UserSession, error)
CreateStockLogs(ctx context.Context, arg CreateStockLogsParams) (StockLog, error)
CreateSuppliers(ctx context.Context, arg CreateSuppliersParams) (Supplier, error)

77
db/sqlc/sale_order.sql.go Normal file
View File

@ -0,0 +1,77 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.17.2
// source: sale_order.sql
package db
import (
"context"
"database/sql"
"github.com/google/uuid"
)
const createSaleOrder = `-- name: CreateSaleOrder :one
INSERT INTO sale_order (
merchant_id,
customer_id,
code,
created_by,
is_paid,
total,
paid_nominal,
note,
change,
is_keep
) VALUES (
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10
)
RETURNING id, index_id, code, created_by, merchant_id, customer_id, is_paid, total, paid_nominal, note, created_at, updated_at, is_keep, change
`
type CreateSaleOrderParams struct {
MerchantID uuid.UUID `json:"merchant_id"`
CustomerID uuid.NullUUID `json:"customer_id"`
Code string `json:"code"`
CreatedBy uuid.UUID `json:"created_by"`
IsPaid bool `json:"is_paid"`
Total float64 `json:"total"`
PaidNominal float64 `json:"paid_nominal"`
Note sql.NullString `json:"note"`
Change float64 `json:"change"`
IsKeep bool `json:"is_keep"`
}
func (q *Queries) CreateSaleOrder(ctx context.Context, arg CreateSaleOrderParams) (SaleOrder, error) {
row := q.db.QueryRowContext(ctx, createSaleOrder,
arg.MerchantID,
arg.CustomerID,
arg.Code,
arg.CreatedBy,
arg.IsPaid,
arg.Total,
arg.PaidNominal,
arg.Note,
arg.Change,
arg.IsKeep,
)
var i SaleOrder
err := row.Scan(
&i.ID,
&i.IndexID,
&i.Code,
&i.CreatedBy,
&i.MerchantID,
&i.CustomerID,
&i.IsPaid,
&i.Total,
&i.PaidNominal,
&i.Note,
&i.CreatedAt,
&i.UpdatedAt,
&i.IsKeep,
&i.Change,
)
return i, err
}

View File

@ -0,0 +1,64 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.17.2
// source: sale_order_detail.sql
package db
import (
"context"
"github.com/google/uuid"
)
const createSaleOrderDetail = `-- name: CreateSaleOrderDetail :one
INSERT INTO sale_order_detail (
sale_order_id,
product_id,
product_name,
quantity,
sub_total,
product_price,
profit
) VALUES(
$1, $2, $3, $4, $5, $6, $7
)
RETURNING id, index_id, sale_order_id, product_id, product_name, quantity, sub_total, product_price, profit, created_at, updated_at
`
type CreateSaleOrderDetailParams struct {
SaleOrderID uuid.UUID `json:"sale_order_id"`
ProductID uuid.UUID `json:"product_id"`
ProductName string `json:"product_name"`
Quantity float64 `json:"quantity"`
SubTotal float64 `json:"sub_total"`
ProductPrice float64 `json:"product_price"`
Profit float64 `json:"profit"`
}
func (q *Queries) CreateSaleOrderDetail(ctx context.Context, arg CreateSaleOrderDetailParams) (SaleOrderDetail, error) {
row := q.db.QueryRowContext(ctx, createSaleOrderDetail,
arg.SaleOrderID,
arg.ProductID,
arg.ProductName,
arg.Quantity,
arg.SubTotal,
arg.ProductPrice,
arg.Profit,
)
var i SaleOrderDetail
err := row.Scan(
&i.ID,
&i.IndexID,
&i.SaleOrderID,
&i.ProductID,
&i.ProductName,
&i.Quantity,
&i.SubTotal,
&i.ProductPrice,
&i.Profit,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}

View File

@ -13,6 +13,7 @@ type Store interface {
Querier
PurchaseOrderTx(ctx context.Context, arg PurchasoOrderTxParams) (PurchaseOrderTxResult, error)
CreateUserMerchantTx(ctx context.Context, arg UserMerchantTxParams) (UserMerchantTxResult, error)
SaleOrderTx(ctx context.Context, arg SaleOrderTxParams) (SaleOrderTxResult, error)
}
type SQLStore struct {
@ -46,6 +47,8 @@ func (store *SQLStore) execTx(ctx context.Context, fn func(*Queries) error) erro
}
// -------- PURCHASE ORDER TRANSACTION --------- //
type PurchaseOrderProduct struct {
ProductID uuid.UUID `json:"product_id"`
Quantity float64 `json:"quantity"`
@ -127,11 +130,11 @@ func (store *SQLStore) PurchaseOrderTx(ctx context.Context, arg PurchasoOrderTxP
MerchantID: arg.MerchantID,
CreatedBy: arg.CreatedBy,
TransactionID: uuid.NullUUID{UUID: result.PurchaseOrder.ID, Valid: true},
TransactionActionType: "Purchase_order",
TransactionDescription: fmt.Sprintf("Penambahan produk %s dari pembelian dengan code %s", product.Name, arg.Code.String),
TransactionActionType: "purchase_order",
TransactionDescription: fmt.Sprintf("Penambahan stok produk %s dari pembelian dengan code %s", product.Name, arg.Code.String),
Type: util.STOCK_LOG_IN,
SellingPrice: product.SellingPrice,
PurchasePrice: product.PurchasePrice,
PurchasePrice: arg.Products[i].Price,
Quantity: arg.Products[i].Quantity,
})
@ -147,6 +150,116 @@ func (store *SQLStore) PurchaseOrderTx(ctx context.Context, arg PurchasoOrderTxP
return result, err
}
// ------ SALE ORDER TRANSACTION ----------- //
type SaleOrderProduct struct {
ProductID uuid.UUID `json:"product_id"`
ProductName string `json:"product_name"`
Quantity float64 `json:"quantity"`
Sub_total float64 `json:"sub_total"`
Price float64 `json:"price"`
Profit float64 `json:"profit"`
}
type SaleOrderTxParams struct {
MerchantID uuid.UUID `json:"merchant_id"`
CreatedBy uuid.UUID `json:"created_by"` // user_id
CustomerID uuid.NullUUID `json:"customer_id"`
Code string `json:"code"`
IsPaid bool `json:"is_paid"`
Total float64 `json:"total"`
PaidNominal float64 `json:"paid_nominal"`
Note sql.NullString `json:"note"`
IsKeep bool `json:"is_keep"`
Change float64 `json:"change"`
Products []SaleOrderProduct `json:"products"`
}
type SaleOrderTxResult struct {
SaleOrder SaleOrder `json:"sale_order"`
SaleOrderDetail []SaleOrderDetail `json:"detail"`
}
func (store *SQLStore) SaleOrderTx(ctx context.Context, arg SaleOrderTxParams) (SaleOrderTxResult, error) {
var result SaleOrderTxResult
err := store.execTx(ctx, func(q *Queries) error {
var err error
result.SaleOrder, err = q.CreateSaleOrder(ctx, CreateSaleOrderParams{
MerchantID: arg.MerchantID,
CustomerID: uuid.NullUUID{Valid: arg.CustomerID.Valid, UUID: arg.CustomerID.UUID},
Code: arg.Code,
CreatedBy: arg.CreatedBy,
IsPaid: arg.IsPaid,
Total: arg.Total,
PaidNominal: arg.PaidNominal,
Note: sql.NullString{Valid: len(arg.Note.String) > 0, String: arg.Note.String},
IsKeep: arg.IsKeep,
})
if err != nil {
return err
}
for i := 0; i < len(arg.Products); i++ {
saleOrderDetail, err := q.CreateSaleOrderDetail(ctx, CreateSaleOrderDetailParams{
SaleOrderID: result.SaleOrder.ID,
ProductID: arg.Products[i].ProductID,
ProductName: arg.Products[i].ProductName,
Quantity: arg.Products[i].Quantity,
SubTotal: arg.Products[i].Sub_total,
ProductPrice: arg.Products[i].Price,
Profit: arg.Products[i].Profit,
})
if err != nil {
return err
}
result.SaleOrderDetail = append(result.SaleOrderDetail, saleOrderDetail)
product, err := q.GetStockForUpdateStock(ctx, arg.Products[i].ProductID)
if err != nil {
return err
}
err = q.UpdateProductStock(ctx, UpdateProductStockParams{
ID: product.ID,
Stock: product.Stock - arg.Products[i].Quantity,
})
if err != nil {
return err
}
_, err = q.CreateStockLogs(ctx, CreateStockLogsParams{
ProductID: arg.Products[i].ProductID,
MerchantID: arg.MerchantID,
CreatedBy: arg.CreatedBy,
TransactionID: uuid.NullUUID{UUID: result.SaleOrder.ID, Valid: true},
TransactionActionType: "sale_order",
TransactionDescription: fmt.Sprintf("Pengurangan stok produk %s dari penjualan dengan code %s", product.Name, arg.Code),
Type: util.STOCK_LOG_OUT,
SellingPrice: arg.Products[i].Price,
PurchasePrice: product.PurchasePrice,
Quantity: arg.Products[i].Quantity,
})
if err != nil {
return err
}
}
return nil
})
return result, err
}
// ---------- CREATE USER MERCHANT TRANSACTION ------------ //
type UserMerchantTxParams struct {
Email string `json:"email"`
Fullname string `json:"fullname"`

View File

@ -6,6 +6,7 @@ import (
"testing"
"git.nochill.in/nochill/naice_pos/util"
"github.com/google/uuid"
"github.com/stretchr/testify/require"
)
@ -13,7 +14,7 @@ func TestPurchaseOrder(t *testing.T) {
var products []PurchaseOrderProduct
store := NewStore(testDB)
user := createRandomUser(t)
user := createRandomUser(t) // TODO: Change to use UserMerchantTx
supplier, _ := createRandomSupplier(t)
product1, _ := createRandomProduct(t)
product2, _ := createRandomProduct(t)
@ -50,8 +51,8 @@ func TestPurchaseOrder(t *testing.T) {
for i := 0; i < tries; i++ {
go func() {
result, err := store.PurchaseOrderTx(context.Background(), PurchasoOrderTxParams{
MerchantID: supplier.MerchantID,
CreatedBy: user.ID,
MerchantID: supplier.MerchantID, // GET FROM UserMerchantTx
CreatedBy: user.ID, // TODO: GET FROM UserMerchantTx
SupplierID: supplier.ID,
Code: sql.NullString{Valid: true, String: util.RandomTransactionCode("P", util.RandomInt(1, 10))},
IsPaid: true,
@ -80,6 +81,82 @@ func TestPurchaseOrder(t *testing.T) {
}
}
func TestCreateSaleOrder(t *testing.T) {
var products []SaleOrderProduct
store := NewStore(testDB)
user := createRandomUser(t)
customer, _ := createRandomCustomer(t)
product1, _ := createRandomProduct(t)
product2, _ := createRandomProduct(t)
errs := make(chan error)
results := make(chan SaleOrderTxResult)
saleProducts1Quantity := util.RandomFloat(1, 99)
saleProducts1Price := util.RandomFloat(999, 9999)
saleProducts1SubTotal := saleProducts1Price * saleProducts1Quantity
saleProducts1Profit := saleProducts1SubTotal - saleProducts1Quantity*product1.SellingPrice
saleProducts2Quantity := util.RandomFloat(1, 99)
saleProducts2Price := util.RandomFloat(999, 9999)
saleProducts2SubTotal := saleProducts2Price * saleProducts2Quantity
saleProducts2Profit := saleProducts2SubTotal - saleProducts2Quantity*product2.SellingPrice
saleProducts_1 := SaleOrderProduct{
ProductID: product1.ID,
Quantity: saleProducts1Quantity,
Sub_total: saleProducts1SubTotal,
Price: saleProducts1Price,
ProductName: product1.Name,
Profit: saleProducts1Profit,
}
saleProducts_2 := SaleOrderProduct{
ProductID: product2.ID,
Quantity: saleProducts2Quantity,
Sub_total: saleProducts2SubTotal,
Price: saleProducts2Price,
ProductName: product2.Name,
Profit: saleProducts2Profit,
}
products = append(products, saleProducts_1, saleProducts_2)
tries := 3
for i := 0; i < tries; i++ {
go func() {
result, err := store.SaleOrderTx(context.Background(), SaleOrderTxParams{
MerchantID: customer.MerchantID,
CreatedBy: user.ID,
CustomerID: uuid.NullUUID{Valid: true, UUID: customer.ID},
Code: util.RandomTransactionCode("S", util.RandomInt(1, 10)),
IsPaid: true,
Total: product1.PurchasePrice + product2.PurchasePrice,
PaidNominal: product1.PurchasePrice + product2.PurchasePrice,
Note: sql.NullString{Valid: true, String: ""},
Products: products,
})
errs <- err
results <- result
}()
}
for i := 0; i < tries; i++ {
err := <-errs
require.NoError(t, err)
result := <-results
require.NotEmpty(t, result)
saleOrder := result.SaleOrder
require.NotEmpty(t, saleOrder)
require.Equal(t, saleOrder.MerchantID, customer.MerchantID) // TODO: Change to use UserMerchantTx result
}
}
func TestCreateUserMerchant(t *testing.T) {
store := NewStore(testDB)

View File

@ -11,7 +11,7 @@ import (
func createRandomSupplier(t *testing.T) (Supplier, CreateSuppliersParams) {
arg := CreateSuppliersParams{
MerchantID: uuid.MustParse("04a1b0a7-69b4-41da-a053-2f6b95c93195"),
MerchantID: uuid.MustParse("54b8a2d9-16be-4239-8828-5daa317028dc"),
Name: util.RandomString(10),
}

8
util/validate.go Normal file
View File

@ -0,0 +1,8 @@
package util
import "github.com/google/uuid"
func IsValidUUID(u string) bool {
_, err := uuid.Parse(u)
return err == nil
}