# Ohda API v1

Base URL:

```text
https://api.example.com/api/v1
```

كل الطلبات والردود JSON. كل المسارات ماعدا `auth/login` و`health` تحتاج:

```text
Authorization: Bearer TOKEN
```

## المصادقة

### `POST /auth/login`

```json
{
  "identifier": "01000000000",
  "password": "strong-password",
  "device_name": "Ahmed Windows"
}
```

### `GET /me`

بيانات المستخدم الحالي ودوره وحالة تغيير كلمة المرور.

### `POST /auth/change-password`

```json
{
  "current_password": "temporary-password",
  "new_password": "new-strong-password"
}
```

## الموظفون — المدير فقط

- `GET /users`
- `POST /users`
- `PATCH /users/{id}/status`
- `POST /users/{id}/reset-password`

إنشاء موظف:

```json
{
  "name": "أحمد",
  "phone": "01011111111",
  "email": "ahmed@example.com",
  "temporary_password": "TempPass-2026",
  "role": "employee"
}
```

لا يوجد endpoint تسجيل ذاتي.

الحد الأدنى لكلمة المرور الجديدة أو المؤقتة هو 8 أحرف.

يرجع إنشاء الموظف بيانات الحساب الأساسية كاملة، ومنها الهاتف والبريد والحالة، حتى يظهر فورًا في قائمة الفريق.

## كشف حساب الموظف

### `GET /users/{id}/statement`

المدير يفتح كشف أي موظف، والموظف لا يستطيع فتح إلا كشفه هو. يدعم فترة وفلتر بيعة اختياريًا:

```text
/users/2/statement?from=2026-08-01&to=2026-08-31&deal_id=4
```

يرجع الأرصدة الحالية، صافي مبيعات الفترة، المرتجعات، التحصيلات، التوريدات، المصروفات، حركات العهدة، المخزون والعملاء. عند تحديد `deal_id` يطبق الفلتر على المخزون والمبيعات والمرتجعات والمصروفات وحركاتها المرتبطة.

### `POST /users/{id}/reconciliations` — المدير فقط

```json
{
  "counted_cash": 12350,
  "notes": "مطابقة نهاية الوردية"
}
```

يحفظ المبلغ المتوقع وقت المطابقة والمبلغ المعدود والفرق؛ الفرق السالب عجز والموجب زيادة، ويرسل إشعارًا للموظف.

## المصروفات

- `GET /expenses`: الموظف يرى مصروفاته فقط، والمدير يرى الجميع، مع فلتر `status` أو `employee_id` للمدير.
- `POST /expenses`: تسجيل مصروف جديد.
- `GET /expenses/{id}`: تفاصيل المصروف وسجل التعويضات.
- `GET /expenses/{id}/receipt`: قراءة الإيصال المرفق بصلاحيات نفس المصروف.
- `POST /expenses/{id}/approve`: اعتماد المدير للمصروف المعلق.
- `POST /expenses/{id}/reject`: رفض مسبب وإعادة مبلغ العهدة إن كان مخصومًا.
- `POST /expenses/{id}/cancel`: إلغاء الموظف لطلبه المعلق.
- `POST /expenses/{id}/reimburse`: تعويض كامل أو جزئي لمصروف دفعه الموظف من ماله.
- `POST /expenses/{id}/reverse`: عكس المصروف المعتمد بسبب إلزامي.

مثال مصروف دفعه الموظف من عهدته:

```json
{
  "deal_id": 4,
  "category": "labor",
  "funding_source": "employee_custody",
  "amount": 850,
  "payment_method": "cash",
  "description": "أجرة عمال تحميل",
  "notes": "تحميل دفعة اللابتوبات"
}
```

القيم المتاحة لـ`funding_source`: `employee_custody` أو `employee_personal`، ويستطيع المدير أيضًا استخدام `company`. الإيصال اختياري ويرسل داخل `receipt` باسم الملف وMIME وBase64، بصيغ JPG/PNG/WEBP/PDF وبحد 5 ميجابايت.

مصروف العهدة يُخصم وقت الإرسال حتى يظل الرصيد الحقيقي صحيحًا؛ الرفض أو الإلغاء يعيد المبلغ تلقائيًا. المصروف الشخصي لا يخصم من العهدة، وبعد الاعتماد يظهر كرصيد مستحق للموظف حتى يسجّل المدير التعويض:

```json
{
  "amount": 500,
  "payment_method": "instapay",
  "notes": "تعويض جزئي"
}
```

## تقارير الأرباح — المدير فقط

### `GET /reports/profitability`

تقرير واحد يجمع مؤشرات الشركة والبيعات والموظفين. يدعم الفترة وفلتر بيعة وموظف:

```text
/reports/profitability?from=2026-08-01&to=2026-08-31&deal_id=4&employee_id=2
```

يرجع:

- إجمالي وصافي المبيعات بعد المرتجعات.
- تكلفة البضاعة المباعة ومجمل الربح والمصروفات وصافي الربح.
- عدد الأصناف المباعة بدون تكلفة، ويجعل `profit_is_complete` بقيمة `false`.
- أداء كل بيعة خلال الفترة والمخزون الحالي والتكلفة الكلية المسجلة.
- نتيجة نهائية لكل بيعة مغلقة من كل إيراداتها ناقص تكلفة الشراء والنقل والمصروفات.
- أداء كل موظف ومبيعاته وتكلفتها ومصروفاته وعهدته وديون عملائه والمبالغ المستحقة له.

التحصيلات والتوريدات لا تُنسب لبيعة بعينها لأنها قد تغطي أكثر من عملية؛ لذلك لا تظهر عند استخدام `deal_id`.

## البيعات والبنود والدفعات

### `POST /deals` — المدير

```json
{
  "name": "بيعة بنك",
  "source": "مزاد البنك الأهلي",
  "purchase_cost": 100000,
  "transport_cost": 5000,
  "items": [
    {"name": "لابتوبات"},
    {"name": "مكاتب"},
    {"name": "كراسي"}
  ]
}
```

- `GET /deals`: المدير يرى الجميع، والموظف يرى المسند له فقط.
- `GET /deals/{id}`
- `POST /deals/{id}/items`
- `PATCH /deals/{id}`: تعديل الاسم والمصدر وتكلفة الشراء والنقل، للبيعات الحالية أو المؤرشفة.
- `PATCH /deals/{id}/status`: إرسال `{"status":"closed"}` للأرشفة أو `{"status":"active"}` للاسترجاع.
- `DELETE /deals/{id}`: حذف البيعة المسودة/المؤرشفة فقط إذا لم ترتبط بإسناد أو دفعة.

الأرشفة تُرفض إذا بقي مخزون متاح أو دفعات لم يكتمل استلامها وجردها. الحذف النهائي محمي للحفاظ على الحسابات وسجل العمليات.

### `POST /deal-items/{id}/batches` — إضافة دفعة لنفس البيعة

```json
{
  "expected_quantity": 50,
  "purchase_cost": 250000,
  "transport_cost": 3500,
  "notes": "دفعة إضافية من نفس البيعة"
}
```

### `POST /assignments`

إسناد البيعة كلها: اترك `deal_item_id` و`batch_id` بدون إرسال.

```json
{
  "deal_id": 1,
  "deal_item_id": 2,
  "batch_id": 4,
  "employee_ids": [2, 3],
  "is_shared": true
}
```

## الاستلام والجرد

### `GET /inventory-tasks`

يعرض للموظف الدفعات المسندة له فقط مع قرار الاستلام وحالة الجرد، ويعرض للمدير جميع مهام الدفعات.

عند إسناد بيعة كاملة أو بند لا يحتوي على دفعات، ينشئ النظام دفعة أولية تلقائيًا حتى تظهر مهمة الاستلام والجرد للموظف. كما تُصلح الإسنادات القديمة غير المرتبطة بدفعة تلقائيًا عند طلب القائمة.

### `POST /batches/{id}/receive`

```json
{"decision":"accepted","notes":"تم الاستلام"}
```

### `POST /batches/{id}/inventory`

```json
{
  "lines": [
    {
      "name": "Laptop Dell Latitude 5420",
      "quantity": 40,
      "unit": "piece",
      "condition_label": "سليم",
      "suggested_price": 14500,
      "unit_cost": 11200
    },
    {
      "name": "Laptop Lenovo",
      "quantity": 10,
      "condition_label": "يحتاج صيانة",
      "suggested_price": 0
    }
  ]
}
```

- `POST /inventories/{id}/submit`
- `POST /inventories/{id}/approve` — المدير
- `POST /inventories/{id}/request-changes` — المدير، body يحتوي `reason`.
- `GET /stock?search=Dell`

المخزون لا يصبح متاحًا للبيع إلا بعد اعتماد الجرد.

## العملاء

- `GET /customers`
- `POST /customers`
- `GET /customers/{id}`
- `PATCH /customers/{id}` — تعديل الاسم والهاتف والعنوان والملاحظات.
- `DELETE /customers/{id}` — حذف آمن مع الاحتفاظ بالحركات القديمة، ويُرفض إذا كان على العميل رصيد.
- `GET /customers/{id}/statement`

الموظف يضيف العميل لحسابه تلقائيًا. المدير يرسل `owner_employee_id`.
الموظف لا يستطيع تعديل أو حذف إلا عملاءه، بينما المدير يستطيع إدارة كل العملاء. إعادة إضافة رقم هاتف لعميل محذوف سابقًا تعيد تنشيط نفس العميل وسجله.

```json
{
  "name": "محمود السيد",
  "phone": "01001234567",
  "address": "القاهرة"
}
```

## المبيعات

### `POST /sales`

بيع مختلط، جزء الآن والباقي آجل:

```json
{
  "customer_id": 1,
  "payment_type": "mixed",
  "payment_method": "cash",
  "paid_now": 10000,
  "due_at": "2026-08-30",
  "lines": [
    {"product_id": 1, "quantity": 2, "unit_price": 14000}
  ]
}
```

## المرتجعات

- `POST /sales/{id}/returns` — المدير أو الموظف صاحب عملية البيع.
- `GET /returns` — الموظف يرى مرتجعات مبيعاته والمدير يرى الجميع.
- `GET /returns/{id}`

```json
{
  "reason": "العميل استبدل الجهاز",
  "refund_account": "employee",
  "refund_method": "cash",
  "lines": [
    {
      "sale_line_id": 12,
      "quantity": 1,
      "stock_disposition": "saleable"
    }
  ]
}
```

`stock_disposition` إما `saleable` لإعادة الصنف للمخزون المتاح، أو `review` لتسجيله كتالف/محتاج مراجعة بدون إتاحته للبيع. الموظف يرد المبلغ من عهدته، والمدير يختار `employee` أو `company`، و`refund_method` يحدد طريقة الرد النقدي إن وجد. تقل مديونية العميل أولًا ثم يُسجل أي مبلغ زائد كرد نقدي.

في طلب المدير يجب إضافة `employee_id`. في بيع الكاش يحسب السيرفر `paid_now` مساويًا للإجمالي. السعر المستخدم هو السعر الفعلي في الطلب، وليس السعر المقترح.

- `GET /sales`
- `GET /sales/{id}`
- `POST /sales/{id}/reverse` — المدير فقط:

```json
{
  "reason": "إلغاء العملية وإرجاع البضاعة",
  "refund_account": "employee"
}
```

## التحصيل

### `POST /collections`

```json
{
  "customer_id": 1,
  "amount": 7000,
  "payment_method": "cash",
  "notes": "دفعة من الحساب"
}
```

يقل دين العميل ويزيد المبلغ في عهدة الموظف. إذا سجل المدير التحصيل بنفسه يدخل المبلغ خزينة الشركة.

- `GET /collections`
- `POST /collections/{id}/reverse` — المدير فقط، مع `reason`.

## التوريد

### `POST /remittances`

```json
{
  "amount": 7000,
  "payment_method": "cash",
  "notes": "توريد اليوم"
}
```

### `POST /remittances/{id}/receive` — المدير

```json
{"amount":6000,"notes":"استلام جزئي"}
```

الاستلام الجزئي مدعوم، ولا ينتقل المبلغ لخزينة الشركة إلا بعد هذا الطلب.

- `GET /remittances/{id}` يعرض دفعات الاستلام الجزئية.
- `POST /remittances/{id}/reject` — المدير فقط، مع `reason`، وقبل استلام أي جزء.

## مبلغ غير مفصل

### `POST /unallocated-amounts`

```json
{
  "amount": 20000,
  "payment_method": "cash",
  "notes": "الموظف قال إن المبلغ معه ولم يرسل تفاصيل البيع"
}
```

بعد تسجيل تفاصيل البيع، اربط المبلغ حتى لا يُحسب مرتين:

### `POST /unallocated-amounts/{id}/settle`

```json
{"sale_id":10,"amount":20000}
```

## الإشعارات ولوحة التحكم

- `GET /dashboard`
- `GET /notifications?unread_only=true`
- `POST /notifications/read`
- `GET /audit-logs?user_id=2&type=sale` — المدير فقط.
- `GET /reports/summary?from=2026-08-01&to=2026-08-31` — المبيعات والتكلفة والربح والسيولة.
- `GET /reports/deals/{id}` — تكلفة وربح ومخزون وموظفو كل بيعة.

```json
{"ids":[1,2,3]}
```

## الأخطاء

```json
{
  "message": "وصف واضح للمشكلة",
  "errors": {
    "field": ["سبب الخطأ"]
  }
}
```

أهم الأكواد: `401` جلسة، `403` صلاحية، `404` غير موجود، `409` تعارض حالة أو مخزون، `422` مدخلات، `429` محاولات دخول كثيرة.
