diff --git a/docs/quick-sales-customer-field-behavior.md b/docs/quick-sales-customer-field-behavior.md
new file mode 100644
index 00000000..f6dbdd4e
--- /dev/null
+++ b/docs/quick-sales-customer-field-behavior.md
@@ -0,0 +1,55 @@
+# رفتار فیلد مشتری در فروش سریع
+
+این سند رفتار جستوجو، انتخاب و ثبت سریع مشتری در صفحهٔ فروش سریع را تعریف میکند. ثبت سریع با `enableQuickCreateOnSubmit` فقط برای نمونههای این فیلد در فروش سریع فعال است؛ سایر مصرفکنندگان `CustomerComboboxWidget` بدون درخواست صریح مشتری جدید ایجاد نمیکنند.
+
+## رفتار Enter
+
+ترتیب تصمیمگیری پس از زدن Enter به شکل زیر است:
+
+1. اگر نتایج متن فعلی هنوز بارگذاری نشدهاند، ابتدا جستوجو کامل میشود.
+2. اگر دقیقاً یک نتیجه وجود داشته باشد، همان مشتری انتخاب میشود.
+3. اگر چند نتیجه وجود داشته باشد و کاربر با جهتنما ردیفی را مشخص کرده باشد، همان ردیف انتخاب میشود.
+4. اگر چند نتیجه وجود داشته باشد و ردیفی صریحاً مشخص نشده باشد، API تطبیق دقیق و نتایج مشابه را بررسی میکند؛ نتیجههای مشابه در همان منوی شناور میمانند تا کاربر با جهتنما و Enter یا موس انتخاب کند.
+5. اگر نتیجهای وجود نداشته باشد، ورودی برای یافتن یا ساختن سریع مشتری به API ارسال میشود.
+
+پاسخ جستوجوی قدیمی اجازه ندارد نتایج متن جدید را جایگزین کند. اولین ردیف نیز صرفاً بهدلیل قرارگرفتن در ابتدای فهرست، انتخابشده محسوب نمیشود.
+
+## قالبهای ورودی ثبت سریع
+
+ورودی میتواند شامل نام، موبایل یا هر دو باشد. ترتیب نام و موبایل مهم نیست و تمام بخش نام بهصورت یکپارچه در `alias_name` قرار میگیرد.
+
+| ورودی | نام مستعار | موبایل |
+|---|---|---|
+| `علی رضایی` | `علی رضایی` | خالی |
+| `09121234567` | `09121234567` | `09121234567` |
+| `09121234567 سید محمد مهدی رضوی` | `سید محمد مهدی رضوی` | `09121234567` |
+| `زهرا سادات موسوی +989121234567` | `زهرا سادات موسوی` | `09121234567` |
+| `۰۹۱۲۱۲۳۴۵۶۷ کاظمی` | `کاظمی` | `09121234567` |
+
+اعداد فارسی و عربی به انگلیسی تبدیل میشوند. قالبهای `09xxxxxxxxx`، `9xxxxxxxxx`، `+989xxxxxxxxx`، `989xxxxxxxxx` و `00989xxxxxxxxx` پشتیبانی میشوند. فاصله و خط تیره داخل شماره نیز پذیرفته میشود. وجود بیش از یک موبایل معتبر، ورودی را مبهم میکند و ثبت خودکار انجام نمیشود.
+
+در ورودی فقط موبایل، بهدلیل الزامیبودن نام مستعار شخص، موبایل استانداردشده بهعنوان نام مستعار نیز ذخیره میشود.
+
+## جلوگیری از ثبت تکراری
+
+API پیش از ایجاد شخص، تمام اشخاص همان کسبوکار را دوباره بررسی میکند:
+
+- موبایل پس از استانداردسازی با فیلدهای موبایل، موبایل دوم، موبایل سوم و تلفن مقایسه میشود.
+- نام با یکسانسازی فاصلهها، نیمفاصله و حروف عربی/فارسی مقایسه میشود.
+- نام مستعار، نام، نام خانوادگی، نام شرکت و ترکیب نام و نام خانوادگی در بررسی تشابه شرکت دارند.
+- اگر یک شخص مشابه پیدا شود، همان شخص انتخاب میشود و رکورد جدید ساخته نمیشود.
+- اگر چند شخص مشابه پیدا شوند، فهرست آنها نمایش داده میشود تا کاربر انتخاب کند.
+- تطبیق موبایل دقیق بر تطبیق نام و تطبیق نام دقیق بر نامهای جزئی اولویت دارد.
+- در PostgreSQL، بررسی و ایجاد سریع برای هر کسبوکار با قفل تراکنشی سریالی میشود تا دو درخواست همزمان رکورد تکراری نسازند.
+
+شخصی که واقعاً جدید باشد با نوع «مشتری» ساخته میشود و بدون بازشدن فرم شخص، برای فاکتور جاری انتخاب میشود.
+
+در پیشنمایش وب محلی، اگر `API_BASE_URL` صریحاً تنظیم نشده باشد، درخواستها به پورت `8000` همان میزبان ارسال میشوند؛ برای نمونه، رابط `192.168.50.101:8080` از API آدرس `192.168.50.101:8000` استفاده میکند. این fallback مانع ارسال اشتباه درخواستها به وبسرور Flutter روی پورت ۸۰۸۰ و دریافت خطای `405 Method Not Allowed` میشود. در محیطهای غیرمحلی، همان origin و reverse proxy برنامه استفاده میشود.
+
+## مسیرهای پیادهسازی
+
+- رابط و رفتار صفحهکلید: `hesabixUI/hesabix_ui/lib/widgets/invoice/customer_combobox_widget.dart`
+- تجزیهٔ نام و موبایل: `hesabixUI/hesabix_ui/lib/utils/customer_quick_entry.dart`
+- فراخوانی API: `hesabixUI/hesabix_ui/lib/services/customer_service.dart`
+- endpoint: `POST /api/v1/customers/quick-resolve`
+- منطق تطبیق و ایجاد: `hesabixAPI/app/services/customer_quick_entry_service.py`
diff --git a/hesabixAPI/adapters/api/v1/customers.py b/hesabixAPI/adapters/api/v1/customers.py
index 76b75e8d..acd95619 100755
--- a/hesabixAPI/adapters/api/v1/customers.py
+++ b/hesabixAPI/adapters/api/v1/customers.py
@@ -3,11 +3,13 @@ from sqlalchemy.orm import Session
from pydantic import BaseModel
from typing import Optional, List
+from adapters.db.models.person import Person
from adapters.db.session import get_db
from app.core.responses import success_response, format_datetime_fields
from app.core.auth_dependency import get_current_user, AuthContext
from app.core.permissions import require_business_access_dep
from app.services.person_service import search_persons, count_persons, get_person_by_id
+from app.services.customer_quick_entry_service import resolve_or_create_quick_customer
router = APIRouter(prefix="/customers", tags=["اشخاص و مشتریان"])
@@ -38,6 +40,63 @@ class CustomerSearchResponse(BaseModel):
has_more: bool
+class CustomerQuickResolveRequest(BaseModel):
+ business_id: int
+ alias_name: Optional[str] = None
+ mobile: Optional[str] = None
+
+
+class CustomerQuickResolveResponse(BaseModel):
+ customers: List[CustomerResponse]
+ created: bool
+
+
+def _customer_response(person: Person) -> CustomerResponse:
+ name_parts = [
+ value
+ for value in (person.alias_name, person.first_name, person.last_name)
+ if value
+ ]
+ return CustomerResponse(
+ id=person.id,
+ name=" ".join(name_parts) if name_parts else "نامشخص",
+ code=str(person.code) if person.code else None,
+ phone=person.phone or person.mobile,
+ email=person.email,
+ address=person.address,
+ is_active=True,
+ created_at=person.created_at.isoformat() if person.created_at else None,
+ )
+
+
+@router.post(
+ "/quick-resolve",
+ summary="یافتن یا ایجاد سریع مشتری",
+ description=(
+ "مشتری موجود را با موبایل یا نام مشابه برمیگرداند و تنها در صورت "
+ "نبود نتیجه، مشتری جدید ایجاد میکند"
+ ),
+ response_model=CustomerQuickResolveResponse,
+)
+async def quick_resolve_customer(
+ request: Request,
+ payload: CustomerQuickResolveRequest,
+ ctx: AuthContext = Depends(get_current_user),
+ db: Session = Depends(get_db),
+ _: None = Depends(require_business_access_dep),
+):
+ resolution = resolve_or_create_quick_customer(
+ db,
+ business_id=payload.business_id,
+ alias_name=payload.alias_name,
+ mobile=payload.mobile,
+ )
+ return CustomerQuickResolveResponse(
+ customers=[_customer_response(person) for person in resolution.persons],
+ created=resolution.created,
+ )
+
+
@router.post("/search",
summary="جستوجوی مشتریها",
description="جستوجو در لیست مشتریها (اشخاص) با قابلیت فیلتر و صفحهبندی",
@@ -98,30 +157,7 @@ async def search_customers(
limit=search_request.limit
)
- # تبدیل به فرمت مشتری
- customers = []
- for person in persons:
- # ساخت نام کامل
- name_parts = []
- if person.alias_name:
- name_parts.append(person.alias_name)
- if person.first_name:
- name_parts.append(person.first_name)
- if person.last_name:
- name_parts.append(person.last_name)
- full_name = " ".join(name_parts) if name_parts else person.alias_name or "نامشخص"
-
- customer = CustomerResponse(
- id=person.id,
- name=full_name,
- code=str(person.code) if person.code else None,
- phone=person.phone or person.mobile,
- email=person.email,
- address=person.address,
- is_active=True, # اشخاص همیشه فعال در نظر گرفته میشوند
- created_at=person.created_at.isoformat() if person.created_at else None
- )
- customers.append(customer)
+ customers = [_customer_response(person) for person in persons]
# محاسبه تعداد کل
total_count = count_persons(
diff --git a/hesabixAPI/adapters/api/v1/kardex.py b/hesabixAPI/adapters/api/v1/kardex.py
index 57fc1aae..557961c5 100755
--- a/hesabixAPI/adapters/api/v1/kardex.py
+++ b/hesabixAPI/adapters/api/v1/kardex.py
@@ -846,7 +846,7 @@ async def export_kardex_pdf_endpoint(
{'گزارش کاردکس' if is_fa else 'Kardex Report'}
{(
"" +
- "".join([f"{escape(str(f.get('label')))}: {escape(str(f.get('value')))}" for f in (filters_summary or [])]) +
+ "".join([f'''{escape(str(f.get('label')))}: {escape(str(f.get('value')))}''' for f in (filters_summary or [])]) +
"
"
) if filters_summary else ""}
diff --git a/hesabixAPI/app/services/customer_quick_entry_service.py b/hesabixAPI/app/services/customer_quick_entry_service.py
new file mode 100644
index 00000000..8a65426d
--- /dev/null
+++ b/hesabixAPI/app/services/customer_quick_entry_service.py
@@ -0,0 +1,206 @@
+"""Resolve or create a customer entered directly in a customer search field."""
+from __future__ import annotations
+
+from dataclasses import dataclass
+import re
+from typing import Iterable
+
+from sqlalchemy import text
+from sqlalchemy.orm import Session
+
+from adapters.api.v1.schema_models.person import (
+ PersonCreateRequest,
+ PersonType as PersonRequestType,
+)
+from adapters.db.models.person import Person
+from app.core.responses import ApiError
+from app.services.person_service import create_person
+from app.services.telephony.phone_normalizer import normalize_iran_phone, numbers_match
+
+
+_NAME_TRANSLATION = str.maketrans({"ي": "ی", "ى": "ی", "ك": "ک", "ۀ": "ه"})
+
+
+@dataclass(frozen=True)
+class QuickCustomerResolution:
+ persons: list[Person]
+ created: bool
+
+
+def clean_customer_alias(value: str | None) -> str | None:
+ if value is None:
+ return None
+ cleaned = value.translate(_NAME_TRANSLATION).replace("", " ")
+ cleaned = " ".join(cleaned.split())
+ return cleaned or None
+
+
+def normalize_customer_alias(value: str | None) -> str:
+ return (clean_customer_alias(value) or "").casefold()
+
+
+def normalize_customer_mobile(value: str | None) -> str | None:
+ if value is None or not str(value).strip():
+ return None
+ canonical = normalize_iran_phone(str(value)).get("canonical")
+ if canonical and re.fullmatch(r"09\d{9}", canonical):
+ return canonical
+ return None
+
+
+def _person_alias_candidates(person: Person) -> set[str]:
+ raw_values = {
+ person.alias_name,
+ person.first_name,
+ person.last_name,
+ person.company_name,
+ " ".join(
+ value
+ for value in (person.alias_name, person.first_name, person.last_name)
+ if value
+ ),
+ " ".join(
+ value for value in (person.first_name, person.last_name) if value
+ ),
+ }
+ return {
+ normalized
+ for normalized in (normalize_customer_alias(value) for value in raw_values)
+ if normalized
+ }
+
+
+def person_matches_quick_customer_entry(
+ person: Person,
+ *,
+ alias_name: str | None,
+ mobile: str | None,
+) -> bool:
+ if mobile:
+ stored_numbers = (
+ person.mobile,
+ person.mobile_2,
+ person.mobile_3,
+ person.phone,
+ )
+ if any(numbers_match(mobile, stored, min_len=10) for stored in stored_numbers):
+ return True
+
+ normalized_alias = normalize_customer_alias(alias_name)
+ if normalized_alias:
+ for candidate in _person_alias_candidates(person):
+ if normalized_alias in candidate or candidate in normalized_alias:
+ return True
+
+ return False
+
+
+def _person_matches_mobile(person: Person, mobile: str) -> bool:
+ stored_numbers = (
+ person.mobile,
+ person.mobile_2,
+ person.mobile_3,
+ person.phone,
+ )
+ return any(numbers_match(mobile, stored, min_len=10) for stored in stored_numbers)
+
+
+def find_quick_customer_matches(
+ persons: Iterable[Person],
+ *,
+ alias_name: str | None,
+ mobile: str | None,
+ limit: int = 20,
+) -> list[Person]:
+ candidates = list(persons)
+
+ # موبایل شناسه قویتری از نام است. اگر پیدا شود، شباهت نام نباید اشخاص
+ # دیگری را وارد نتیجه کند و جلوی انتخاب خودکار مشتری موجود را بگیرد.
+ if mobile:
+ mobile_matches = [
+ person for person in candidates if _person_matches_mobile(person, mobile)
+ ]
+ if mobile_matches:
+ return mobile_matches[:limit]
+
+ normalized_alias = normalize_customer_alias(alias_name)
+ if not normalized_alias:
+ return []
+
+ exact_matches: list[Person] = []
+ similar_matches: list[Person] = []
+ for person in candidates:
+ aliases = _person_alias_candidates(person)
+ if normalized_alias in aliases:
+ exact_matches.append(person)
+ elif any(
+ normalized_alias in candidate or candidate in normalized_alias
+ for candidate in aliases
+ ):
+ similar_matches.append(person)
+
+ # نام دقیق نیز قبل از نتایج جزئی انتخاب میشود. چند رکورد دقیق همچنان برای
+ # انتخاب صریح کاربر برگردانده میشوند و رکورد تازه ساخته نمیشود.
+ return (exact_matches or similar_matches)[:limit]
+
+
+def resolve_or_create_quick_customer(
+ db: Session,
+ *,
+ business_id: int,
+ alias_name: str | None,
+ mobile: str | None,
+) -> QuickCustomerResolution:
+ cleaned_alias = clean_customer_alias(alias_name)
+ normalized_mobile = normalize_customer_mobile(mobile)
+ if mobile and not normalized_mobile:
+ raise ApiError("INVALID_CUSTOMER_MOBILE", "شماره موبایل معتبر نیست")
+ if not cleaned_alias and not normalized_mobile:
+ raise ApiError(
+ "CUSTOMER_QUICK_ENTRY_REQUIRED",
+ "نام مشتری یا شماره موبایل را وارد کنید",
+ )
+
+ # درخواستهای همزمان یک کسبوکار نباید بین بررسی تکراری و ایجاد فاصله بیفتند.
+ bind = db.get_bind()
+ if bind is not None and bind.dialect.name == "postgresql":
+ db.execute(
+ text("SELECT pg_advisory_xact_lock(:namespace, :business_id)"),
+ {"namespace": 1129665364, "business_id": int(business_id)},
+ )
+
+ persons = (
+ db.query(Person)
+ .filter(Person.business_id == int(business_id))
+ .order_by(Person.alias_name, Person.id)
+ .all()
+ )
+ matches = find_quick_customer_matches(
+ persons,
+ alias_name=cleaned_alias,
+ mobile=normalized_mobile,
+ )
+ if matches:
+ return QuickCustomerResolution(persons=matches, created=False)
+
+ stored_alias = cleaned_alias or normalized_mobile
+ assert stored_alias is not None
+ result = create_person(
+ db,
+ int(business_id),
+ PersonCreateRequest(
+ alias_name=stored_alias,
+ mobile=normalized_mobile,
+ person_types=[PersonRequestType.CUSTOMER],
+ ),
+ )
+ person_id = int(result["data"]["id"])
+ created = (
+ db.query(Person)
+ .filter(
+ Person.business_id == int(business_id),
+ Person.id == person_id,
+ )
+ .one()
+ )
+ return QuickCustomerResolution(persons=[created], created=True)
diff --git a/hesabixAPI/tests/test_customer_quick_entry_service.py b/hesabixAPI/tests/test_customer_quick_entry_service.py
new file mode 100644
index 00000000..a3f4929d
--- /dev/null
+++ b/hesabixAPI/tests/test_customer_quick_entry_service.py
@@ -0,0 +1,116 @@
+from types import SimpleNamespace
+
+from app.services.customer_quick_entry_service import (
+ clean_customer_alias,
+ find_quick_customer_matches,
+ normalize_customer_alias,
+ normalize_customer_mobile,
+)
+
+
+def _person(
+ person_id: int,
+ alias_name: str,
+ *,
+ mobile: str | None = None,
+ first_name: str | None = None,
+ last_name: str | None = None,
+):
+ return SimpleNamespace(
+ id=person_id,
+ alias_name=alias_name,
+ first_name=first_name,
+ last_name=last_name,
+ company_name=None,
+ mobile=mobile,
+ mobile_2=None,
+ mobile_3=None,
+ phone=None,
+ )
+
+
+def test_normalizes_customer_alias_and_mobile_variants():
+ assert clean_customer_alias(" علي رضايي ") == "علی رضایی"
+ assert normalize_customer_alias("علي رضايي") == "علی رضایی"
+ assert normalize_customer_mobile("+98 912 123 4567") == "09121234567"
+ assert normalize_customer_mobile("۰۹۱۲۱۲۳۴۵۶۷") == "09121234567"
+
+
+def test_finds_existing_person_by_mobile_before_creation():
+ persons = [_person(1, "علی رضایی", mobile="+989121234567")]
+ matches = find_quick_customer_matches(
+ persons,
+ alias_name=None,
+ mobile="09121234567",
+ )
+ assert [person.id for person in matches] == [1]
+
+
+def test_finds_similar_normalized_alias():
+ persons = [
+ _person(1, "علی رضایی"),
+ _person(2, "زهرا موسوی"),
+ ]
+ matches = find_quick_customer_matches(
+ persons,
+ alias_name="علي",
+ mobile=None,
+ )
+ assert [person.id for person in matches] == [1]
+
+
+def test_finds_name_split_between_person_fields():
+ persons = [
+ _person(
+ 1,
+ "محمدرضا",
+ first_name="محمدرضا",
+ last_name="حسینی",
+ )
+ ]
+ matches = find_quick_customer_matches(
+ persons,
+ alias_name="محمدرضا حسینی",
+ mobile=None,
+ )
+ assert [person.id for person in matches] == [1]
+
+
+def test_mobile_match_takes_priority_over_similar_names():
+ persons = [
+ _person(1, "علی رضایی", mobile="09121234567"),
+ _person(2, "علی رضایی فروشگاه"),
+ ]
+ matches = find_quick_customer_matches(
+ persons,
+ alias_name="علی رضایی",
+ mobile="+989121234567",
+ )
+ assert [person.id for person in matches] == [1]
+
+
+def test_exact_name_takes_priority_over_partial_names():
+ persons = [
+ _person(1, "علی رضایی فروشگاه"),
+ _person(2, "علی رضایی"),
+ ]
+ matches = find_quick_customer_matches(
+ persons,
+ alias_name="علی رضایی",
+ mobile=None,
+ )
+ assert [person.id for person in matches] == [2]
+
+
+def test_returns_all_similar_names_when_there_is_no_exact_match():
+ persons = [
+ _person(1, "علی رضایی"),
+ _person(2, "علی احمدی"),
+ _person(3, "زهرا موسوی"),
+ ]
+ matches = find_quick_customer_matches(
+ persons,
+ alias_name="علی",
+ mobile=None,
+ )
+ assert [person.id for person in matches] == [1, 2]
diff --git a/hesabixUI/hesabix_ui/lib/config/app_config.dart b/hesabixUI/hesabix_ui/lib/config/app_config.dart
index 728a4597..1a4e3cd8 100755
--- a/hesabixUI/hesabix_ui/lib/config/app_config.dart
+++ b/hesabixUI/hesabix_ui/lib/config/app_config.dart
@@ -15,14 +15,38 @@ class AppConfig {
/// روی reverse proxy مسیرهای `/api/*` و `/ws/*` به بکاند پاس داده شوند).
/// - در غیر وب، پیشفرض `http://localhost:8000` است.
static String get apiBaseUrl {
- final v = _envApiBaseUrl.trim();
- if (v.isNotEmpty) return v;
+ return resolveApiBaseUrl(
+ configuredValue: _envApiBaseUrl,
+ isWebBuild: kIsWeb,
+ currentUri: Uri.base,
+ );
+ }
- if (kIsWeb) {
- final u = Uri.base;
+ static String resolveApiBaseUrl({
+ required String configuredValue,
+ required bool isWebBuild,
+ required Uri currentUri,
+ }) {
+ final value = configuredValue.trim();
+ if (value.isNotEmpty) return value;
+
+ if (isWebBuild) {
+ final host = currentUri.host.toLowerCase();
+ final isLocalPreview =
+ host == 'localhost' || host == '127.0.0.1' || currentUri.port == 8080;
+ if (isLocalPreview) {
+ // Flutter web-server روی 8080 فقط فایل استاتیک سرو میکند. فرستادن
+ // درخواستهای API به همان origin خطا میدهد. این تشخیص شامل IP شبکه
+ // نیز هست تا پیشنمایش روی دستگاه دیگری API همان میزبان را ببیند.
+ return Uri(
+ scheme: currentUri.scheme,
+ host: currentUri.host,
+ port: 8000,
+ ).origin;
+ }
// مثال: http://localhost:8080 یا https://arc.hesabix.ir
// در این حالت انتظار داریم reverse proxy مسیرهای api/ws را route کند.
- return u.origin;
+ return currentUri.origin;
}
return 'http://localhost:8000';
@@ -53,5 +77,3 @@ class AppConfig {
return '$b/login?reset_token=${Uri.encodeComponent(token)}';
}
}
-
-
diff --git a/hesabixUI/hesabix_ui/lib/pages/business/quick_sales_page.dart b/hesabixUI/hesabix_ui/lib/pages/business/quick_sales_page.dart
index 59fc99cb..bed9db80 100755
--- a/hesabixUI/hesabix_ui/lib/pages/business/quick_sales_page.dart
+++ b/hesabixUI/hesabix_ui/lib/pages/business/quick_sales_page.dart
@@ -3588,6 +3588,7 @@ class _QuickSalesPageState extends State
businessId: widget.businessId,
authStore: widget.authStore,
isRequired: false,
+ enableQuickCreateOnSubmit: true,
label: 'مشتری',
hintText: 'مشتری ناشناس',
),
@@ -4471,6 +4472,7 @@ class _QuickSalesPageState extends State
businessId: widget.businessId,
authStore: widget.authStore,
isRequired: false,
+ enableQuickCreateOnSubmit: true,
label: 'مشتری',
hintText: 'مشتری ناشناس',
),
@@ -4874,6 +4876,7 @@ class _QuickSalesPageState extends State
businessId: widget.businessId,
authStore: widget.authStore,
isRequired: false,
+ enableQuickCreateOnSubmit: true,
dense: true,
label: 'مشتری',
hintText: 'مشتری ناشناس',
diff --git a/hesabixUI/hesabix_ui/lib/services/customer_service.dart b/hesabixUI/hesabix_ui/lib/services/customer_service.dart
index 9b8a8fbc..0df95784 100755
--- a/hesabixUI/hesabix_ui/lib/services/customer_service.dart
+++ b/hesabixUI/hesabix_ui/lib/services/customer_service.dart
@@ -32,7 +32,7 @@ class CustomerService {
if (response.statusCode == 200) {
final data = response.data as Map;
-
+
// تبدیل لیست مشتریها
final customersJson = data['customers'] as List;
final customers = customersJson
@@ -54,6 +54,30 @@ class CustomerService {
}
}
+ /// مشتری موجود را برمیگرداند یا پس از بررسی تکراری، مشتری جدید میسازد.
+ Future