-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: viewsets and models for wpp-cloud catalog and product
- Loading branch information
Showing
20 changed files
with
1,430 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
import requests | ||
|
||
from marketplace.clients.exceptions import CustomAPIException | ||
|
||
|
||
class RequestClient: | ||
def make_request( | ||
self, url: str, method: str, headers=None, data=None, params=None, files=None | ||
): | ||
response = requests.request( | ||
method=method, | ||
url=url, | ||
headers=headers, | ||
json=data, | ||
timeout=60, | ||
params=params, | ||
files=files, | ||
) | ||
if response.status_code >= 500: | ||
raise CustomAPIException(status_code=response.status_code) | ||
elif response.status_code >= 400: | ||
raise CustomAPIException( | ||
detail=response.json() if response.text else response.text, | ||
status_code=response.status_code, | ||
) | ||
|
||
return response | ||
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
from rest_framework.exceptions import APIException | ||
|
||
|
||
class CustomAPIException(APIException): | ||
def __init__(self, detail=None, code=None, status_code=None): | ||
super().__init__(detail, code) | ||
self.status_code = status_code or self.status_code | ||
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,217 @@ | ||
import time | ||
import json | ||
|
||
from django.conf import settings | ||
|
||
from marketplace.clients.base import RequestClient | ||
|
||
WHATSAPP_VERSION = settings.WHATSAPP_VERSION | ||
ACCESS_TOKEN = settings.WHATSAPP_SYSTEM_USER_ACCESS_TOKEN | ||
|
||
|
||
class FacebookAuthorization: | ||
BASE_URL = f"https://graph.facebook.com/{WHATSAPP_VERSION}/" | ||
|
||
def __init__(self): | ||
self.access_token = ACCESS_TOKEN | ||
|
||
def _get_headers(self): | ||
headers = {"Authorization": f"Bearer {self.access_token}"} | ||
return headers | ||
|
||
@property | ||
def get_url(self): | ||
return self.BASE_URL | ||
|
||
|
||
class FacebookClient(FacebookAuthorization, RequestClient): | ||
def create_catalog(self, business_id, name, category=None): | ||
url = self.get_url + f"{business_id}/owned_product_catalogs" | ||
data = {"name": name} | ||
if category: | ||
data["vertical"] = category | ||
|
||
headers = self._get_headers() | ||
response = self.make_request(url, method="POST", headers=headers, data=data) | ||
|
||
return response.json() | ||
|
||
def destroy_catalog(self, catalog_id): | ||
url = self.get_url + f"{catalog_id}" | ||
|
||
headers = self._get_headers() | ||
response = self.make_request(url, method="DELETE", headers=headers) | ||
|
||
return response.json().get("success") | ||
|
||
def create_product_feed(self, product_catalog_id, name): | ||
url = self.get_url + f"{product_catalog_id}/product_feeds" | ||
|
||
data = {"name": name} | ||
headers = self._get_headers() | ||
response = self.make_request(url, method="POST", headers=headers, data=data) | ||
|
||
return response.json() | ||
|
||
def upload_product_feed(self, feed_id, file): | ||
url = self.get_url + f"{feed_id}/uploads" | ||
|
||
headers = self._get_headers() | ||
files = { | ||
"file": ( | ||
file.name, | ||
file, | ||
file.content_type, | ||
) | ||
} | ||
response = self.make_request(url, method="POST", headers=headers, files=files) | ||
return response.json() | ||
|
||
def create_product_feed_via_url( | ||
self, product_catalog_id, name, feed_url, file_type, interval, hour | ||
): # TODO: adjust this method | ||
url = self.get_url + f"{product_catalog_id}/product_feeds" | ||
|
||
schedule = {"interval": interval, "url": feed_url, "hour": str(hour)} | ||
|
||
data = {"name": name, "schedule": json.dumps(schedule), "file_type": file_type} | ||
|
||
headers = self._get_headers() | ||
response = self.make_request(url, method="POST", headers=headers, data=data) | ||
return response.json() | ||
|
||
def get_upload_status(self, feed_id, max_attempts=10, wait_time=30): | ||
""" | ||
Checks the upload status using long polling. | ||
Args: | ||
upload_id (str): The ID of the upload. | ||
max_attempts (int): Maximum number of polling attempts. Default is 10. | ||
wait_time (int): Wait time in seconds between polling attempts. Default is 30 seconds. | ||
Returns: | ||
bool or str: True if 'end_time' is found, otherwise a formatted error message. | ||
""" | ||
url = self.get_url + f"{feed_id}/uploads" | ||
headers = self._get_headers() | ||
|
||
attempts = 0 | ||
while attempts < max_attempts: | ||
response = self.make_request(url, method="GET", headers=headers) | ||
data = response.json() | ||
|
||
if data.get("data") and data["data"][0].get("end_time"): | ||
return True | ||
|
||
time.sleep(wait_time) | ||
attempts += 1 | ||
|
||
total_wait_time = wait_time * max_attempts | ||
return ( | ||
f"Unable to retrieve the upload completion status for feed {feed_id}. " | ||
f"Waited for a total of {total_wait_time} seconds." | ||
) | ||
|
||
def list_products_by_feed(self, feed_id): | ||
url = self.get_url + f"{feed_id}/products" | ||
|
||
headers = self._get_headers() | ||
response = self.make_request(url, method="GET", headers=headers) | ||
|
||
return response.json() | ||
|
||
def list_all_products_by_feed(self, feed_id): | ||
url = self.get_url + f"{feed_id}/products" | ||
headers = self._get_headers() | ||
all_products = [] | ||
|
||
while url: | ||
response = self.make_request(url, method="GET", headers=headers).json() | ||
all_products.extend(response.get("data", [])) | ||
url = response.get("paging", {}).get("next") | ||
|
||
return all_products | ||
|
||
def list_all_catalogs(self, wa_business_id): | ||
url = self.get_url + f"{wa_business_id}/owned_product_catalogs" | ||
headers = self._get_headers() | ||
all_catalog_ids = [] | ||
|
||
while url: | ||
response = self.make_request(url, method="GET", headers=headers).json() | ||
catalog_data = response.get("data", []) | ||
all_catalog_ids.extend([item["id"] for item in catalog_data]) | ||
url = response.get("paging", {}).get("next") | ||
|
||
return all_catalog_ids | ||
|
||
def destroy_feed(self, feed_id): | ||
url = self.get_url + f"{feed_id}" | ||
|
||
headers = self._get_headers() | ||
response = self.make_request(url, method="DELETE", headers=headers) | ||
|
||
return response.json().get("success") | ||
|
||
def get_connected_catalog(self, waba_id): | ||
url = self.get_url + f"{waba_id}/product_catalogs" | ||
headers = self._get_headers() | ||
response = self.make_request(url, method="GET", headers=headers) | ||
return response.json() | ||
|
||
def enable_catalog(self, waba_id, catalog_id): | ||
url = self.get_url + f"{waba_id}/product_catalogs" | ||
data = {"catalog_id": catalog_id} | ||
headers = self._get_headers() | ||
response = self.make_request(url, method="POST", headers=headers, data=data) | ||
return response.json() | ||
|
||
def disable_catalog(self, waba_id, catalog_id): | ||
url = self.get_url + f"{waba_id}/product_catalogs" | ||
data = {"catalog_id": catalog_id, "method": "delete"} | ||
headers = self._get_headers() | ||
response = self.make_request(url, method="POST", headers=headers, data=data) | ||
return response.json() | ||
|
||
def get_catalog_details(self, catalog_id): | ||
url = self.get_url + f"{catalog_id}" | ||
params = {"fields": "name,vertical"} | ||
headers = self._get_headers() | ||
response = self.make_request(url, method="GET", headers=headers, params=params) | ||
|
||
return response.json() | ||
|
||
def _update_commerce_settings(self, wa_phone_number_id, **settings): | ||
url = self.BASE_URL + f"{wa_phone_number_id}/whatsapp_commerce_settings" | ||
headers = self._get_headers() | ||
response = self.make_request(url, method="POST", headers=headers, data=settings) | ||
return response.json() | ||
|
||
def toggle_cart(self, wa_phone_number_id, enable=True): | ||
return self._update_commerce_settings( | ||
wa_phone_number_id, is_cart_enabled=enable | ||
) | ||
|
||
def toggle_catalog_visibility(self, wa_phone_number_id, make_visible=True): | ||
return self._update_commerce_settings( | ||
wa_phone_number_id, is_catalog_visible=make_visible | ||
) | ||
|
||
def get_wpp_commerce_settings(self, wa_phone_number_id): | ||
""" | ||
Returns: | ||
"data": [ | ||
{ | ||
"is_cart_enabled": true, | ||
"is_catalog_visible": true, | ||
"id": "270925148880242" | ||
} | ||
] | ||
Or: | ||
"data": [] | ||
""" | ||
url = self.BASE_URL + f"{wa_phone_number_id}/whatsapp_commerce_settings" | ||
|
||
headers = self._get_headers() | ||
response = self.make_request(url, method="GET", headers=headers) | ||
return response.json() | ||
Empty file.
77 changes: 77 additions & 0 deletions
77
marketplace/core/types/channels/whatsapp_cloud/services/facebook_service.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,77 @@ | ||
from marketplace.clients.facebook.client import FacebookClient | ||
from marketplace.wpp_products.models import Catalog | ||
|
||
|
||
class FacebookService: | ||
def __init__(self): | ||
self.client = FacebookClient() | ||
|
||
def get_app_facebook_credentials(self, app): | ||
wa_business_id = app.config.get("wa_business_id") | ||
wa_waba_id = app.config.get("wa_waba_id") | ||
wa_phone_number_id = app.config.get("wa_phone_number_id") | ||
|
||
if not wa_business_id or not wa_waba_id or not wa_phone_number_id: | ||
raise ValueError( | ||
"Not found 'wa_waba_id', 'wa_business_id' or wa_phone_number_id in app.config " | ||
) | ||
return { | ||
"wa_business_id": wa_business_id, | ||
"wa_waba_id": wa_waba_id, | ||
"wa_phone_number_id": wa_phone_number_id, | ||
} | ||
|
||
def catalog_creation(self, validated_data, app, user): | ||
business_id = self.get_app_facebook_credentials(app=app).get("wa_business_id") | ||
response = self.client.create_catalog( | ||
business_id, validated_data["name"], validated_data["category"] | ||
) | ||
|
||
if response and response.get("id"): | ||
catalog = Catalog.objects.create( | ||
app=app, | ||
facebook_catalog_id=response.get("id"), | ||
name=validated_data["name"], | ||
created_by=user, | ||
) | ||
return catalog, response.get("id") | ||
|
||
return None, None | ||
|
||
def catalog_deletion(self, catalog): | ||
return self.client.destroy_catalog(catalog.facebook_catalog_id) | ||
|
||
def enable_catalog(self, catalog): | ||
waba_id = self.get_app_facebook_credentials(app=catalog.app).get("wa_waba_id") | ||
return self.client.enable_catalog( | ||
waba_id=waba_id, catalog_id=catalog.facebook_catalog_id | ||
) | ||
|
||
def disable_catalog(self, catalog): | ||
waba_id = self.get_app_facebook_credentials(app=catalog.app).get("wa_waba_id") | ||
return self.client.disable_catalog( | ||
waba_id=waba_id, catalog_id=catalog.facebook_catalog_id | ||
) | ||
|
||
def get_connected_catalog(self, app): | ||
waba_id = self.get_app_facebook_credentials(app=app).get("wa_waba_id") | ||
response = self.client.get_connected_catalog(waba_id=waba_id) | ||
return response.get("data")[0].get("id") if response else [] | ||
|
||
def toggle_cart(self, app, enable=True): | ||
business_phone_number_id = self.get_app_facebook_credentials(app=app).get( | ||
"wa_phone_number_id" | ||
) | ||
return self.client.toggle_cart(business_phone_number_id, enable) | ||
|
||
def toggle_catalog_visibility(self, app, visible=True): | ||
business_phone_number_id = self.get_app_facebook_credentials(app=app).get( | ||
"wa_phone_number_id" | ||
) | ||
return self.client.toggle_catalog_visibility(business_phone_number_id, visible) | ||
|
||
def wpp_commerce_settings(self, app): | ||
business_phone_number_id = self.get_app_facebook_credentials(app=app).get( | ||
"wa_phone_number_id" | ||
) | ||
return self.client.get_wpp_commerce_settings(business_phone_number_id) | ||
44 changes: 44 additions & 0 deletions
44
marketplace/core/types/channels/whatsapp_cloud/services/flows_service.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
from marketplace.flows.client import FlowsClient | ||
|
||
|
||
class FlowsService: | ||
def __init__(self): | ||
self.client = FlowsClient() | ||
|
||
def _update_flows_config(self, app): | ||
""" | ||
synchronize Flows channel configuration. | ||
""" | ||
detail_channel = self.client.detail_channel(app.flow_object_uuid) | ||
flows_config = detail_channel["config"] | ||
flows_config["catalogs"] = app.config["catalogs"] | ||
self.client.update_config( | ||
data=flows_config, flow_object_uuid=app.flow_object_uuid | ||
) | ||
|
||
def update_app_and_flows_with_catalog(self, app, catalog, catalog_id): | ||
if "catalogs" not in app.config: | ||
app.config["catalogs"] = [] | ||
|
||
app.config["catalogs"].append({"facebook_catalog_id": catalog_id}) | ||
app.save() | ||
|
||
self._update_flows_config(app) | ||
|
||
return catalog | ||
|
||
def remove_catalog_from_app(self, catalog): | ||
if "catalogs" in catalog.app.config: | ||
catalogs_to_remove = [ | ||
idx | ||
for idx, catalog_entry in enumerate(catalog.app.config["catalogs"]) | ||
if catalog_entry.get("facebook_catalog_id") | ||
== catalog.facebook_catalog_id | ||
] | ||
|
||
for idx in reversed(catalogs_to_remove): | ||
del catalog.app.config["catalogs"][idx] | ||
|
||
catalog.app.save() | ||
|
||
self._update_flows_config(catalog.app) | ||
Oops, something went wrong.