Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,14 @@ bin/
docs/
web/

.env.example
.env.test
.env
.env.*
.gitignore
commercify.db
cookies.txt
docker-compose.local.yml
docker-compose.yml
readme.md
SECURITY.md
todo.txt
Makefile
tygo.yaml
9 changes: 8 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,13 @@ EMAIL_SMTP_HOST=
EMAIL_SMTP_PORT=587
EMAIL_SMTP_USERNAME=
EMAIL_SMTP_PASSWORD=
# EMAIL_FROM_ADDRESS, in most cases should be the same as EMAIL_SMTP_USERNAME
EMAIL_FROM_ADDRESS=
EMAIL_FROM_NAME=Commercify
# The email the should receive notification when new orders are made.
EMAIL_ADMIN_ADDRESS=dev@zenfulcode.com
# Contact email address for support, used in emails
EMAIL_CONTACT_ADDRESS=dev@commercify.app

STRIPE_ENABLED=true
STRIPE_SECRET_KEY=sk_test_your_key
Expand All @@ -42,4 +46,7 @@ MOBILEPAY_WEBHOOK_URL=https://your-site.com/api/webhooks/mobilepay
MOBILEPAY_PAYMENT_DESCRIPTION=Commercify Store Purchase

RETURN_URL=https://your-site.com/payment/complete
CORS_ALLOWED_ORIGINS=http://localhost:3000,http://localhost:5173
CORS_ALLOWED_ORIGINS=http://localhost:3000,http://localhost:5173

# Used in emails and UI
STORE_NAME=Commercify Store
11 changes: 9 additions & 2 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ type EmailConfig struct {
FromEmail string
FromName string
AdminEmail string
ContactEmail string // Contact email for customer support
StoreName string // Store name used in emails
Enabled bool
}

Expand Down Expand Up @@ -142,7 +144,7 @@ func LoadConfig() (*Config, error) {
enabledProviders = append(enabledProviders, "mobilepay")
}

return &Config{
config := Config{
Server: ServerConfig{
Port: getEnv("SERVER_PORT", "6091"),
ReadTimeout: readTimeout,
Expand Down Expand Up @@ -173,6 +175,7 @@ func LoadConfig() (*Config, error) {
FromEmail: getEnv("EMAIL_FROM_ADDRESS", "noreply@example.com"),
FromName: getEnv("EMAIL_FROM_NAME", "Commercify Store"),
AdminEmail: getEnv("EMAIL_ADMIN_ADDRESS", "admin@example.com"),
StoreName: getEnv("STORE_NAME", "Commercify Store"),
Enabled: emailEnabled,
},
Stripe: StripeConfig{
Expand All @@ -199,7 +202,11 @@ func LoadConfig() (*Config, error) {
AllowAllOrigins: true,
},
DefaultCurrency: getEnv("DEFAULT_CURRENCY", "USD"),
}, nil
}

config.Email.ContactEmail = getEnv("EMAIL_CONTACT_ADDRESS", config.Email.FromEmail)

return &config, nil
}

// getEnv gets an environment variable or returns a default value
Expand Down
86 changes: 86 additions & 0 deletions config/config_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package config

import (
"os"
"testing"
)

func TestEmailConfigDefaults(t *testing.T) {
// Clear any existing environment variables
envVars := []string{
"EMAIL_CONTACT_ADDRESS",
"STORE_NAME",
"EMAIL_FROM_ADDRESS",
"EMAIL_FROM_NAME",
}

originalValues := make(map[string]string)
for _, envVar := range envVars {
originalValues[envVar] = os.Getenv(envVar)
os.Unsetenv(envVar)
}

// Restore environment variables after test
defer func() {
for envVar, value := range originalValues {
if value != "" {
os.Setenv(envVar, value)
}
}
}()

config, err := LoadConfig()
if err != nil {
t.Fatalf("Failed to load config: %v", err)
}

// Test that ContactEmail falls back to FromEmail when not set
expectedContactEmail := "noreply@example.com" // This is the default FromEmail
if config.Email.ContactEmail != expectedContactEmail {
t.Errorf("Expected ContactEmail to be %s, got %s", expectedContactEmail, config.Email.ContactEmail)
}

// Test that StoreName falls back to FromName when not set
expectedStoreName := "Commercify Store" // This is the default FromName
if config.Email.StoreName != expectedStoreName {
t.Errorf("Expected StoreName to be %s, got %s", expectedStoreName, config.Email.StoreName)
}
}

func TestEmailConfigCustomValues(t *testing.T) {
// Set custom environment variables
os.Setenv("EMAIL_CONTACT_ADDRESS", "support@custom.com")
os.Setenv("STORE_NAME", "Custom Store")
os.Setenv("EMAIL_FROM_ADDRESS", "from@custom.com")
os.Setenv("EMAIL_FROM_NAME", "Custom From Name")

// Clean up after test
defer func() {
os.Unsetenv("EMAIL_CONTACT_ADDRESS")
os.Unsetenv("STORE_NAME")
os.Unsetenv("EMAIL_FROM_ADDRESS")
os.Unsetenv("EMAIL_FROM_NAME")
}()

config, err := LoadConfig()
if err != nil {
t.Fatalf("Failed to load config: %v", err)
}

// Test that custom values are used
if config.Email.ContactEmail != "support@custom.com" {
t.Errorf("Expected ContactEmail to be support@custom.com, got %s", config.Email.ContactEmail)
}

if config.Email.StoreName != "Custom Store" {
t.Errorf("Expected StoreName to be Custom Store, got %s", config.Email.StoreName)
}

if config.Email.FromEmail != "from@custom.com" {
t.Errorf("Expected FromEmail to be from@custom.com, got %s", config.Email.FromEmail)
}

if config.Email.FromName != "Custom From Name" {
t.Errorf("Expected FromName to be Custom From Name, got %s", config.Email.FromName)
}
}
84 changes: 84 additions & 0 deletions docs/RESTAPI.md
Original file line number Diff line number Diff line change
Expand Up @@ -641,6 +641,90 @@ PUT /api/admin/orders/{orderId}/status

Update order status (admin only).

**Request Body:**

```json
{
"status": "shipped"
}
```

**Response Body:**

```json
{
"id": 1,
"order_number": "ORD-001",
"currency": "USD",
"user_id": 1,
"total_amount": 9999,
"status": "shipped",
"payment_status": "captured",
"created_at": "2025-07-10T10:00:00Z",
"updated_at": "2025-07-10T11:00:00Z"
}
```

**Status Codes:**

- `200 OK`: Order status updated successfully
- `400 Bad Request`: Invalid request body or status
- `401 Unauthorized`: Not authenticated
- `403 Forbidden`: Not authorized (not an admin)
- `404 Not Found`: Order not found

**Valid Order Statuses:**

- `pending`: Order is pending
- `paid`: Order has been paid
- `shipped`: Order has been shipped (triggers shipment email)
- `cancelled`: Order has been cancelled
- `completed`: Order is completed

#### Update Order Status with Tracking

```plaintext
PUT /api/admin/orders/{orderId}/status-with-tracking
```

Update order status with optional tracking information (admin only). When updating status to "shipped" with tracking information, an enhanced shipment email will be sent to the customer.

**Request Body:**

```json
{
"status": "shipped",
"tracking_number": "1Z999AA1234567890",
"tracking_url": "https://www.ups.com/track?loc=en_US&tracknum=1Z999AA1234567890"
}
```

**Response Body:**

```json
{
"id": 1,
"order_number": "ORD-001",
"currency": "USD",
"user_id": 1,
"total_amount": 9999,
"status": "shipped",
"payment_status": "captured",
"created_at": "2025-07-10T10:00:00Z",
"updated_at": "2025-07-10T11:00:00Z"
}
```

**Status Codes:**

- `200 OK`: Order status updated successfully
- `400 Bad Request`: Invalid request body or status
- `401 Unauthorized`: Not authenticated
- `403 Forbidden`: Not authorized (not an admin)
- `404 Not Found`: Order not found

**Note:** When an order status is changed to "shipped", an email notification is automatically sent to the customer. If tracking information is provided, it will be included in the email with a tracking link.

### Checkout Management

#### List Checkouts
Expand Down
100 changes: 100 additions & 0 deletions docs/order_api_examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -465,3 +465,103 @@ Example response:
- `403 Forbidden`: User not authorized (not an admin)
- `404 Not Found`: Order not found
- `500 Internal Server Error`: Failed to update order status

## Order Shipment Email Notification Examples

### Update Order Status with Tracking Information

```plaintext
PUT /api/admin/orders/{orderId}/status-with-tracking
```

Update order status with optional tracking information. When an order status is changed to "shipped", an email notification is automatically sent to the customer.

**Path Parameters:**

- `orderId` (required): Order ID

**Request Body for Basic Shipment:**

```json
{
"status": "shipped"
}
```

**Request Body with Tracking Information:**

```json
{
"status": "shipped",
"tracking_number": "1Z999AA1234567890",
"tracking_url": "https://www.ups.com/track?loc=en_US&tracknum=1Z999AA1234567890"
}
```

**Response Body:**

```json
{
"success": true,
"data": {
"id": 123,
"order_number": "ORD-20250707-123",
"user_id": 1,
"status": "shipped",
"payment_status": "captured",
"currency": "USD",
"total_amount": 7498,
"shipping_cost": 1499,
"tax_amount": 0,
"discount_amount": 0,
"created_at": "2024-03-20T11:00:00Z",
"updated_at": "2024-03-20T14:30:00Z"
}
}
```

**Email Notification Details:**

When the order status is updated to "shipped":

1. **Customer Email**: An automated email is sent to the customer's email address containing:

- Order details and summary
- Shipping address
- Expected delivery information
- Tracking number and link (if provided)
- Professional styling with shipping-themed design

2. **Email Template**: Uses `templates/emails/order_shipped.html`

3. **Email Subject**: "Your Order #[OrderID] Has Been Shipped! 📦"

4. **Tracking Integration**: If tracking number and URL are provided, the email includes:

- Clickable tracking link
- Visual tracking information section
- Instructions for tracking the package

5. **Guest Order Support**: Works for both registered users and guest orders using customer details

**Example Email Content Features:**

- Green header indicating successful shipment
- Shipping status badge
- Tracking number in code format for easy copying
- Professional tracking button (if URL provided)
- Complete order summary
- Shipping address confirmation
- Expected delivery timeline
- Contact information for support

**Status Codes:**

- `200 OK`: Order status updated successfully and email sent
- `400 Bad Request`: Invalid order status or tracking information
- `401 Unauthorized`: User not authenticated
- `403 Forbidden`: User not authorized (not an admin)
- `404 Not Found`: Order not found
- `500 Internal Server Error`: Failed to update order status or send email

**Note**: Email sending failures are logged but do not prevent the order status update from succeeding. The system ensures order status changes are persisted even if email delivery fails.
Loading