├── .env.dist
├── .flake8
├── .github
└── workflows
│ └── django.yml
├── .gitignore
├── Makefile
├── Procfile
├── Procfile.dev
├── README.md
├── affiliate
├── __init__.py
├── apps.py
├── dao.py
├── filters.py
├── migrations
│ └── __init__.py
├── models.py
├── serializers.py
├── sql
│ ├── daily_report.sql
│ ├── goal_report.sql
│ ├── offer_report.sql
│ └── sub_report.sql
├── tests
│ ├── __init__.py
│ ├── test_conversion.py
│ ├── test_offer.py
│ └── test_stats.py
├── urls.py
└── views
│ ├── __init__.py
│ ├── conversions.py
│ ├── offers.py
│ ├── profile.py
│ ├── register.py
│ └── stats.py
├── api
├── __init__.py
├── permissions.py
├── tests
│ ├── __init__.py
│ ├── test_advertiser.py
│ ├── test_conversion.py
│ ├── test_landing.py
│ ├── test_offer.py
│ ├── test_offer_traffic_source.py
│ └── test_payout.py
├── urls.py
└── views
│ ├── __init__.py
│ ├── advertiser.py
│ ├── conversions.py
│ ├── landing.py
│ ├── offer.py
│ ├── offer_traffic_source.py
│ └── payout.py
├── app.json
├── dictionaries
├── __init__.py
├── apps.py
├── urls.py
└── views
│ ├── __init__.py
│ ├── categories.py
│ └── countries.py
├── docker-compose.yml
├── docker
└── Dockerfile
├── docs
├── api-spec.png
├── index.html
└── schema.yml
├── ext
├── ipapi
│ ├── __init__.py
│ └── api.py
└── ipstack
│ ├── __init__.py
│ └── api.py
├── manage.py
├── mypy.ini
├── network
├── __init__.py
├── apps.py
├── dao.py
├── migrations
│ └── __init__.py
├── sql
│ ├── affiliate_report.sql
│ ├── daily_report.sql
│ └── offer_report.sql
├── tests
│ ├── __init__.py
│ ├── test_affiliate.py
│ ├── test_conversion.py
│ ├── test_offer.py
│ └── test_stats.py
├── urls.py
└── views
│ ├── __init__.py
│ ├── affiliates.py
│ ├── conversions.py
│ ├── offers.py
│ └── stats.py
├── offer
├── __init__.py
├── admin.py
├── apps.py
├── migrations
│ ├── 0001_initial.py
│ ├── 0002_offer_countries.py
│ ├── 0003_offer_description.py
│ ├── 0004_auto_20191009_1712.py
│ ├── 0005_category.py
│ ├── 0006_trafficsource.py
│ ├── 0007_offer_categories.py
│ ├── 0008_auto_20200618_1647.py
│ ├── 0009_auto_20200618_1706.py
│ ├── 0010_currency.py
│ ├── 0011_payout.py
│ ├── 0012_auto_20200618_1922.py
│ ├── 0013_offer_preview_link.py
│ ├── 0014_advertiser.py
│ ├── 0015_offer_advertiser.py
│ ├── 0016_auto_20200629_2215.py
│ ├── 0017_offer_icon.py
│ ├── 0018_auto_20200726_1815.py
│ ├── 0019_offer_description_html.py
│ ├── 0020_landing.py
│ ├── 0021_auto_20200903_2149.py
│ └── __init__.py
├── models.py
└── tasks
│ ├── __init__.py
│ └── cache_offers.py
├── permissions.txt
├── postback
├── __init__.py
├── admin.py
├── apps.py
├── migrations
│ ├── 0001_initial.py
│ ├── 0002_log.py
│ ├── 0003_auto_20200827_1244.py
│ ├── 0004_auto_20201018_0101.py
│ └── __init__.py
├── models.py
├── tasks
│ ├── __init__.py
│ └── send_postback.py
└── tests
│ ├── __init__.py
│ └── test_send_postback.py
├── project
├── __init__.py
├── _celery.py
├── redis_conn.py
├── settings
│ ├── __init__.py
│ ├── base.py
│ ├── local.dist.py
│ └── prod.py
├── urls.py
└── wsgi.py
├── pytest.ini
├── requirements.dev.txt
├── requirements.txt
├── tracker
├── __init__.py
├── admin.py
├── apps.py
├── dao.py
├── migrations
│ ├── 0001_initial.py
│ ├── 0002_auto_20200618_1524.py
│ ├── 0003_auto_20200629_2057.py
│ ├── 0004_conversion_goal.py
│ ├── 0005_conversion_currency.py
│ ├── 0006_conversion_comment.py
│ ├── 0007_auto_20200803_1648.py
│ ├── 0008_auto_20200803_1822.py
│ ├── 0009_auto_20200803_1824.py
│ ├── 0010_auto_20200803_1825.py
│ ├── 0011_auto_20200826_2206.py
│ ├── 0012_auto_20200914_1037.py
│ ├── 0013_auto_20201018_0101.py
│ └── __init__.py
├── models.py
├── signals.py
├── tasks
│ ├── __init__.py
│ ├── click.py
│ ├── conversion.py
│ └── sync.py
├── tests
│ ├── __init__.py
│ ├── test_click_task.py
│ ├── test_click_view.py
│ ├── test_conversion_task.py
│ ├── test_find_payout.py
│ └── test_postback_view.py
├── urls.py
└── views.py
└── user_profile
├── __init__.py
├── admin.py
├── apps.py
├── migrations
├── 0001_initial.py
├── 0002_profile_manager.py
├── 0003_auto_20200617_2157.py
└── __init__.py
├── models.py
├── signals.py
└── tests
├── __init__.py
├── test_profile.py
└── test_signal.py
/.env.dist:
--------------------------------------------------------------------------------
1 | REDIS_URL=redis://redis
2 | TRACKER_URL=http://0.0.0.0:8000
3 | IPSTACK_TOKEN=xxx
--------------------------------------------------------------------------------
/.flake8:
--------------------------------------------------------------------------------
1 | [flake8]
2 | filename = *.py
3 | exclude = venv,.git,__pycache,migrations
4 | max-line-length = 79
5 | max-complexity = 10
--------------------------------------------------------------------------------
/.github/workflows/django.yml:
--------------------------------------------------------------------------------
1 | name: Django CI
2 |
3 | on:
4 | push:
5 | branches: [ "*" ]
6 | pull_request:
7 | branches: [ "*" ]
8 |
9 | jobs:
10 | build:
11 |
12 | runs-on: ubuntu-22.04
13 | strategy:
14 | max-parallel: 4
15 | matrix:
16 | db: [postgres]
17 | python-version: ["3.10"]
18 |
19 |
20 | services:
21 |
22 | postgres:
23 | image: postgres:10
24 | env:
25 | POSTGRES_DB: postgres
26 | POSTGRES_USER: postgres
27 | POSTGRES_PASSWORD: postgres
28 | options: >-
29 | --health-cmd pg_isready
30 | --health-interval 10s
31 | --health-timeout 5s
32 | --health-retries 5
33 | ports:
34 | - 5432:5432
35 |
36 | redis:
37 | # Docker Hub image
38 | image: redis
39 | # Set health checks to wait until redis has started
40 | options: >-
41 | --health-cmd "redis-cli ping"
42 | --health-interval 10s
43 | --health-timeout 5s
44 | --health-retries 5
45 | ports:
46 | - 6379:6379
47 |
48 |
49 | steps:
50 | - uses: actions/checkout@v3
51 | - name: Set up Python ${{ matrix.python-version }}
52 | uses: actions/setup-python@v3
53 | with:
54 | python-version: ${{ matrix.python-version }}
55 | - name: Install Dependencies
56 | run: |
57 | python -m pip install --upgrade pip
58 | pip install -r requirements.txt
59 | - name: Flake8
60 | run: flake8 .
61 | - name: Run Tests
62 | run: python manage.py test
63 | env:
64 | DJ_SECRET_KEY: xxx
65 | DATABASE_URL: postgresql://postgres:postgres@127.0.0.1/postgres
66 | REDIS_URL: redis://127.0.0.1
67 | TRACKER_URL: https://example.com
68 | IPSTACK_TOKEN: xxx
69 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | develop-eggs/
12 | dist/
13 | downloads/
14 | eggs/
15 | .eggs/
16 | lib/
17 | lib64/
18 | parts/
19 | sdist/
20 | var/
21 | wheels/
22 | *.egg-info/
23 | .installed.cfg
24 | *.egg
25 |
26 | # PyInstaller
27 | # Usually these files are written by a python script from a template
28 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
29 | *.manifest
30 | *.spec
31 |
32 | # Installer logs
33 | pip-log.txt
34 | pip-delete-this-directory.txt
35 |
36 | # Unit test / coverage reports
37 | htmlcov/
38 | .tox/
39 | .coverage
40 | .coverage.*
41 | .cache
42 | nosetests.xml
43 | coverage.xml
44 | *.cover
45 | .hypothesis/
46 | .pytest_cache
47 |
48 | # Translations
49 | *.mo
50 | *.pot
51 |
52 | # Django stuff:
53 | *.log
54 | local_settings.py
55 |
56 | # Flask stuff:
57 | instance/
58 | .webassets-cache
59 |
60 | # Scrapy stuff:
61 | .scrapy
62 |
63 | # Sphinx documentation
64 | docs/_build/
65 |
66 | # PyBuilder
67 | target/
68 |
69 | # Jupyter Notebook
70 | .ipynb_checkpoints
71 |
72 | # pyenv
73 | .python-version
74 |
75 | # celery beat schedule file
76 | celerybeat-schedule
77 | celerybeat.pid
78 |
79 | # SageMath parsed files
80 | *.sage.py
81 |
82 | # Environments
83 | .env
84 | .venv
85 | env/
86 | venv/
87 | ENV/
88 |
89 | # Spyder project settings
90 | .spyderproject
91 | .spyproject
92 |
93 | # Rope project settings
94 | .ropeproject
95 |
96 | # mkdocs documentation
97 | /site
98 |
99 | # mypy
100 | .mypy_cache/
101 |
102 | #IDE
103 | .vscode
104 | .idea/
105 |
106 | # Docker Compose
107 | volumes
108 |
109 | # Project settings
110 | /project/settings/local.py
111 |
112 | sandbox
113 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | .PHONY: makemigrations
2 | makemigrations:
3 | docker-compose run --rm web python manage.py makemigrations
4 |
5 |
6 | .PHONY: migrate
7 | migrate:
8 | docker-compose run --rm web python manage.py migrate
9 |
10 |
11 | .PHONY: dshell
12 | dshell:
13 | docker-compose run --rm web python manage.py shell
14 |
15 |
16 | .PHONY: flake
17 | flake:
18 | docker-compose run --rm web flake8 . | grep -v migrations | grep -v 'dao.py'
19 |
20 |
21 | .PHONY: pytest
22 | pytest:
23 | docker-compose run --rm web pytest
24 |
25 |
26 | .PHONY: test
27 | test:
28 | docker-compose run --rm web python manage.py test
29 |
30 |
31 | .PHONY: mypy
32 | mypy:
33 | docker-compose run --rm web mypy .
34 |
35 |
36 | .PHONY: psql
37 | psql:
38 | docker-compose run --rm postgres psql --host=postgres --dbname=postgres --username=postgres
39 |
--------------------------------------------------------------------------------
/Procfile:
--------------------------------------------------------------------------------
1 | web: gunicorn project.wsgi
2 | worker: celery -A project._celery:_celery worker -l INFO
3 | beat: celery -A project._celery:_celery beat -l INFO
--------------------------------------------------------------------------------
/Procfile.dev:
--------------------------------------------------------------------------------
1 | web: python manage.py runserver 0.0.0.0:80
2 | worker: celery -A project._celery:_celery worker -l INFO
3 | beat: celery -A project._celery:_celery beat -l INFO
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [![telegram_badge]][telegram_link]
2 |
3 |
10 |
11 | # About
12 |
13 | [](https://cpanova.github.io/cpa-network/index.html)
14 |
15 | Backend for CPA Network
16 |
17 | Keywords: Affiliate Tracking Software, CPA Platform, Affiliate Management Platform
18 |
19 | # Features
20 |
21 | - Affiliate Registration
22 | - Offer Management
23 | - Conversion Import
24 | - Reports
25 |
26 |
27 |
28 |
29 | [telegram_badge]: https://img.shields.io/badge/telegram-252850?style=plastic&logo=telegram
30 | [telegram_link]: https://t.me/bloogrox
31 |
32 | # Documentation
33 |
34 | [API Documentation](https://cpanova.github.io/cpa-network/index.html)
35 |
36 | # Development
37 |
38 | - Copy `project/settings/local.dist.py` to `project/settings/local.py`
39 | - Copy `.env.dist` to `.env`
40 |
41 | ```
42 | docker compose up
43 | ```
44 |
45 | django admin: http://0.0.0.0:8000/admin/login/
46 |
47 | API spec: http://0.0.0.0:8000/api/
48 |
49 | # Contribute
50 |
51 | Take part in short [survey](https://forms.gle/fCU6NYjuY8A1J8xd6) (5 questions, no email)
--------------------------------------------------------------------------------
/affiliate/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cpanova/cpa-network/1b5d1fbb3dfaa64441ee0be505b9eb75b44cb659/affiliate/__init__.py
--------------------------------------------------------------------------------
/affiliate/apps.py:
--------------------------------------------------------------------------------
1 | from django.apps import AppConfig
2 |
3 |
4 | class AffiliateConfig(AppConfig):
5 | name = 'affiliate'
6 |
7 | # def ready(self):
8 | # super(AffiliateConfig, self).ready()
9 | # import affiliate.signals # noqa: F401
10 |
--------------------------------------------------------------------------------
/affiliate/dao.py:
--------------------------------------------------------------------------------
1 | import os
2 | from datetime import datetime
3 | from typing import List, Dict, Any
4 | from django.db import connection
5 |
6 |
7 | def daily_report(
8 | user_id: int, start_date: datetime, end_date: datetime,
9 | offer_id: int = 0) -> list:
10 |
11 | offer_filter_clause = ""
12 |
13 | if offer_id:
14 | offer_filter_clause = f"AND offer_id = {offer_id}"
15 |
16 | filepath = os.path.join(
17 | os.path.dirname(__file__), 'sql', 'daily_report.sql'
18 | )
19 | with open(filepath, 'r') as f:
20 | sql = f.read().format(**locals())
21 |
22 | colnames = [
23 | 'date',
24 |
25 | 'clicks',
26 |
27 | 'total_qty',
28 | 'approved_qty',
29 | 'hold_qty',
30 | 'rejected_qty',
31 |
32 | 'cr',
33 |
34 | 'total_payout',
35 | 'approved_payout',
36 | 'hold_payout',
37 | 'rejected_payout'
38 | ]
39 |
40 | with connection.cursor() as cursor:
41 | cursor.execute(sql)
42 | data = cursor.fetchall()
43 | data = [dict(zip(colnames, row)) for row in data]
44 |
45 | return data
46 |
47 |
48 | def offer_report(
49 | user_id: int, start_date: datetime,
50 | end_date: datetime) -> List[Dict[str, Any]]:
51 |
52 | colnames = [
53 | 'offer_id',
54 | 'offer_title',
55 |
56 | 'clicks',
57 |
58 | 'total_qty',
59 | 'approved_qty',
60 | 'hold_qty',
61 | 'rejected_qty',
62 |
63 | 'cr',
64 |
65 | 'total_payout',
66 | 'approved_payout',
67 | 'hold_payout',
68 | 'rejected_payout'
69 | ]
70 |
71 | filepath = os.path.join(
72 | os.path.dirname(__file__), 'sql', 'offer_report.sql'
73 | )
74 | with open(filepath, 'r') as f:
75 | sql = f.read().format(**locals())
76 |
77 | with connection.cursor() as cursor:
78 | cursor.execute(sql)
79 | data = cursor.fetchall()
80 | data = [dict(zip(colnames, row)) for row in data]
81 |
82 | return data
83 |
84 |
85 | def goal_report(
86 | user_id: int, start_date: datetime, end_date: datetime) -> list:
87 |
88 | colnames = [
89 | 'goal_id',
90 | 'goal_name',
91 |
92 | 'total_qty',
93 | 'approved_qty',
94 | 'hold_qty',
95 | 'rejected_qty',
96 |
97 | 'total_payout',
98 | 'approved_payout',
99 | 'hold_payout',
100 | 'rejected_payout'
101 | ]
102 |
103 | filepath = os.path.join(
104 | os.path.dirname(__file__), 'sql', 'goal_report.sql'
105 | )
106 | with open(filepath, 'r') as f:
107 | sql = f.read().format(**locals())
108 |
109 | with connection.cursor() as cursor:
110 | cursor.execute(sql)
111 | data = cursor.fetchall()
112 | data = [dict(zip(colnames, row)) for row in data]
113 |
114 | return data
115 |
116 |
117 | def report_bysub(
118 | sub_index: int, offer_id: int, user_id: int,
119 | start_date: datetime, end_date: datetime) -> list:
120 |
121 | colnames = [
122 | 'sub',
123 |
124 | 'clicks',
125 |
126 | 'total_qty',
127 | 'approved_qty',
128 | 'hold_qty',
129 | 'rejected_qty',
130 |
131 | 'cr',
132 |
133 | 'total_payout',
134 | 'approved_payout',
135 | 'hold_payout',
136 | 'rejected_payout'
137 | ]
138 |
139 | offer_filter_clause = f" AND offer_id = {offer_id} "
140 |
141 | filepath = os.path.join(
142 | os.path.dirname(__file__), 'sql', 'sub_report.sql'
143 | )
144 | with open(filepath, 'r') as f:
145 | sql = f.read().format(**locals())
146 |
147 | with connection.cursor() as cursor:
148 | cursor.execute(sql)
149 | data = cursor.fetchall()
150 | data = [dict(zip(colnames, row)) for row in data]
151 |
152 | return data
153 |
--------------------------------------------------------------------------------
/affiliate/filters.py:
--------------------------------------------------------------------------------
1 | from django_filters import Filter
2 | from django_filters.constants import EMPTY_VALUES
3 |
4 |
5 | class CommaSeparatedTextFilter(Filter):
6 | """Custom filter allow set multiple values to field filter.
7 |
8 | Note: it use case-sensitive search when choosing from list,
9 | you should use pre-annotate to lowercase and lookup __in in annotation
10 | """
11 |
12 | def filter(self, qs, value):
13 |
14 | # if field filled, - cplit by
15 | if value not in EMPTY_VALUES:
16 | value = value.split(',')
17 |
18 | # set lookup expression to choose in list or to default iexact
19 | # it'll be used in superclass
20 | self.lookup_expr = isinstance(value, list) and 'in' or 'iexact'
21 |
22 | return super(CommaSeparatedTextFilter, self).filter(qs, value)
23 |
--------------------------------------------------------------------------------
/affiliate/migrations/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cpanova/cpa-network/1b5d1fbb3dfaa64441ee0be505b9eb75b44cb659/affiliate/migrations/__init__.py
--------------------------------------------------------------------------------
/affiliate/models.py:
--------------------------------------------------------------------------------
1 | # from django.contrib.auth import get_user_model
2 | # from django.db import models
3 |
4 |
5 | # class Affiliate(models.Model):
6 | # user = models.OneToOneField(get_user_model(), on_delete=models.CASCADE)
7 |
8 | # def __str__(self):
9 | # return self.user.username
10 |
--------------------------------------------------------------------------------
/affiliate/serializers.py:
--------------------------------------------------------------------------------
1 | from rest_framework import serializers
2 | from django.contrib.auth import get_user_model
3 | from rest_framework.exceptions import ValidationError
4 |
5 |
6 | class UserSerializer(serializers.ModelSerializer):
7 | password = serializers.CharField(write_only=True)
8 | confirm_password = serializers.CharField(write_only=True)
9 |
10 | def create(self, validated_data):
11 | if validated_data["password"] != validated_data["confirm_password"]:
12 | raise ValidationError(
13 | {'confirm_password': ['Wrong confirm password']}
14 | )
15 |
16 | user = get_user_model().objects.create(
17 | username=validated_data['username']
18 | )
19 | user.set_password(validated_data['password'])
20 | user.is_staff = False
21 | user.is_active = True
22 | user.save()
23 |
24 | return user
25 |
26 | class Meta:
27 | model = get_user_model()
28 | fields = ('id', 'username', 'password', 'confirm_password',)
29 | write_only_fields = ('password',)
30 | read_only_fields = ('id',)
31 |
--------------------------------------------------------------------------------
/affiliate/sql/daily_report.sql:
--------------------------------------------------------------------------------
1 | (SELECT
2 | COALESCE(cl.day, cv.day),
3 | COALESCE(cl.clicks, 0),
4 | COALESCE(cv.total_qty, 0),
5 | COALESCE(cv.approved_qty, 0),
6 | COALESCE(cv.hold_qty, 0),
7 | COALESCE(cv.rejected_qty, 0),
8 | COALESCE(
9 | case cl.clicks
10 | when 0 then 0 -- avoid divizion by zero
11 | else (100 * cv.total_qty / cl.clicks)
12 | end
13 | , 0) AS cr,
14 | COALESCE(cv.total_payout, 0),
15 | COALESCE(cv.approved_payout, 0),
16 | COALESCE(cv.hold_payout, 0),
17 | COALESCE(cv.rejected_payout, 0)
18 | FROM
19 | (
20 | SELECT
21 | created_at::date AS day,
22 | count(*) AS clicks
23 | FROM tracker_click
24 | WHERE
25 | affiliate_id = {user_id}
26 | AND created_at between '{start_date}' AND '{end_date}'
27 | {offer_filter_clause}
28 | GROUP BY day
29 | ) AS cl
30 | FULL OUTER JOIN
31 | (
32 | SELECT
33 | created_at::date AS day,
34 | count(*) AS total_qty,
35 | count(*) FILTER (WHERE status = 'approved') AS approved_qty,
36 | count(*) FILTER (WHERE status = 'hold') AS hold_qty,
37 | count(*) FILTER (WHERE status = 'rejected') AS rejected_qty,
38 | sum(payout) AS total_payout,
39 | sum(payout) FILTER (WHERE status = 'approved') AS approved_payout,
40 | sum(payout) FILTER (WHERE status = 'hold') AS hold_payout,
41 | sum(payout) FILTER (WHERE status = 'rejected') AS rejected_payout
42 | FROM tracker_conversion
43 | WHERE
44 | affiliate_id = {user_id}
45 | AND created_at between '{start_date}' AND '{end_date}'
46 | {offer_filter_clause}
47 | GROUP BY day
48 | ) AS cv
49 | ON cl.day = cv.day
50 | ORDER BY cl.day DESC)
51 | ;
--------------------------------------------------------------------------------
/affiliate/sql/goal_report.sql:
--------------------------------------------------------------------------------
1 | SELECT
2 | report.goal_id,
3 | goal.name,
4 | COALESCE(report.total_qty, 0),
5 | COALESCE(report.approved_qty, 0),
6 | COALESCE(report.hold_qty, 0),
7 | COALESCE(report.rejected_qty, 0),
8 | COALESCE(report.total_payout, 0),
9 | COALESCE(report.approved_payout, 0),
10 | COALESCE(report.hold_payout, 0),
11 | COALESCE(report.rejected_payout, 0)
12 | FROM (
13 | SELECT
14 | goal_id,
15 | count(*) AS total_qty,
16 | count(*) FILTER (WHERE status = 'approved') AS approved_qty,
17 | count(*) FILTER (WHERE status = 'hold') AS hold_qty,
18 | count(*) FILTER (WHERE status = 'rejected') AS rejected_qty,
19 | sum(payout) AS total_payout,
20 | sum(payout) FILTER (WHERE status = 'approved') AS approved_payout,
21 | sum(payout) FILTER (WHERE status = 'hold') AS hold_payout,
22 | sum(payout) FILTER (WHERE status = 'rejected') AS rejected_payout
23 | FROM tracker_conversion
24 | WHERE
25 | affiliate_id = {user_id}
26 | AND created_at between '{start_date}' AND '{end_date}'
27 | GROUP BY goal_id
28 | ) AS report
29 | LEFT JOIN offer_goal AS goal
30 | ON report.goal_id = goal.id
31 | ;
--------------------------------------------------------------------------------
/affiliate/sql/offer_report.sql:
--------------------------------------------------------------------------------
1 | SELECT
2 | report.offer_id,
3 | o.title,
4 | COALESCE(report.clicks, 0),
5 | COALESCE(report.total_qty, 0),
6 | COALESCE(report.approved_qty, 0),
7 | COALESCE(report.hold_qty, 0),
8 | COALESCE(report.rejected_qty, 0),
9 | COALESCE(report.cr, 0),
10 | COALESCE(report.total_payout, 0),
11 | COALESCE(report.approved_payout, 0),
12 | COALESCE(report.hold_payout, 0),
13 | COALESCE(report.rejected_payout, 0)
14 | FROM
15 | (
16 | SELECT
17 | COALESCE(cl.offer_id, cv.offer_id) as offer_id,
18 | cl.clicks,
19 | cv.total_qty,
20 | cv.approved_qty,
21 | cv.hold_qty,
22 | cv.rejected_qty,
23 | case cl.clicks
24 | when 0 then 0 -- avoid divizion by zero
25 | else (100 * cv.total_qty / cl.clicks)
26 | end AS cr,
27 | cv.total_payout,
28 | cv.approved_payout,
29 | cv.hold_payout,
30 | cv.rejected_payout
31 | FROM
32 | (
33 | SELECT
34 | offer_id,
35 | count(*) as clicks
36 | FROM tracker_click
37 | WHERE
38 | affiliate_id = {user_id}
39 | AND created_at between '{start_date}' AND '{end_date}'
40 | GROUP BY offer_id
41 | ) AS cl
42 | FULL OUTER JOIN
43 | (
44 | SELECT
45 | offer_id,
46 | count(*) AS total_qty,
47 | count(*) FILTER (WHERE status = 'approved') AS approved_qty,
48 | count(*) FILTER (WHERE status = 'hold') AS hold_qty,
49 | count(*) FILTER (WHERE status = 'rejected') AS rejected_qty,
50 | sum(payout) AS total_payout,
51 | sum(payout) FILTER (WHERE status = 'approved') AS approved_payout,
52 | sum(payout) FILTER (WHERE status = 'hold') AS hold_payout,
53 | sum(payout) FILTER (WHERE status = 'rejected') AS rejected_payout
54 | FROM tracker_conversion
55 | WHERE
56 | affiliate_id = {user_id}
57 | AND created_at between '{start_date}' AND '{end_date}'
58 | GROUP BY offer_id
59 | ) AS cv
60 | ON cl.offer_id = cv.offer_id
61 | ) AS report
62 | LEFT JOIN offer_offer AS o
63 | ON report.offer_id = o.id
64 | ;
--------------------------------------------------------------------------------
/affiliate/sql/sub_report.sql:
--------------------------------------------------------------------------------
1 | (SELECT
2 | COALESCE(cl.sub, cv.sub),
3 | COALESCE(cl.clicks, 0),
4 | COALESCE(cv.total_qty, 0),
5 | COALESCE(cv.approved_qty, 0),
6 | COALESCE(cv.hold_qty, 0),
7 | COALESCE(cv.rejected_qty, 0),
8 | COALESCE(
9 | case cl.clicks
10 | when 0 then 0 -- avoid divizion by zero
11 | else (100 * cv.total_qty / cl.clicks)
12 | end
13 | , 0) AS cr,
14 | COALESCE(cv.total_payout, 0),
15 | COALESCE(cv.approved_payout, 0),
16 | COALESCE(cv.hold_payout, 0),
17 | COALESCE(cv.rejected_payout, 0)
18 | FROM
19 | (
20 | SELECT
21 | sub{sub_index} AS sub,
22 | count(*) AS clicks
23 | FROM tracker_click
24 | WHERE
25 | affiliate_id = {user_id}
26 | AND created_at between '{start_date}' AND '{end_date}'
27 | {offer_filter_clause}
28 | GROUP BY sub
29 | ) AS cl
30 | FULL OUTER JOIN
31 | (
32 | SELECT
33 | sub{sub_index} AS sub,
34 | count(*) AS total_qty,
35 | count(*) FILTER (WHERE status = 'approved') AS approved_qty,
36 | count(*) FILTER (WHERE status = 'hold') AS hold_qty,
37 | count(*) FILTER (WHERE status = 'rejected') AS rejected_qty,
38 | sum(payout) AS total_payout,
39 | sum(payout) FILTER (WHERE status = 'approved') AS approved_payout,
40 | sum(payout) FILTER (WHERE status = 'hold') AS hold_payout,
41 | sum(payout) FILTER (WHERE status = 'rejected') AS rejected_payout
42 | FROM tracker_conversion
43 | WHERE
44 | affiliate_id = {user_id}
45 | AND created_at between '{start_date}' AND '{end_date}'
46 | {offer_filter_clause}
47 | GROUP BY sub
48 | ) AS cv
49 | ON cl.sub = cv.sub
50 | ORDER BY cl.sub ASC)
51 | ;
--------------------------------------------------------------------------------
/affiliate/tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cpanova/cpa-network/1b5d1fbb3dfaa64441ee0be505b9eb75b44cb659/affiliate/tests/__init__.py
--------------------------------------------------------------------------------
/affiliate/tests/test_conversion.py:
--------------------------------------------------------------------------------
1 | import uuid
2 | import pytz
3 | import datetime
4 | from rest_framework.test import APITestCase
5 | from django.contrib.auth import get_user_model
6 | from offer.models import Offer
7 | from tracker.models import Conversion
8 |
9 |
10 | class ConversionTestCase(APITestCase):
11 | list_url = '/affiliate/conversions/'
12 |
13 | def setUp(self):
14 | super(ConversionTestCase, self).setUp()
15 | credentials = {
16 | 'username': 'test@test.com',
17 | 'password': '1234',
18 | }
19 | self.user = get_user_model().objects.create_user(**credentials)
20 | self.client.login(**credentials)
21 | self.offer = Offer.objects.create(
22 | title='blabla',
23 | description='blabla'
24 | )
25 | self.conversions = [
26 | Conversion.objects.create(
27 | click_id=uuid.uuid4(),
28 | click_date=datetime.datetime.now(pytz.UTC),
29 | revenue=0.0,
30 | payout=0.0,
31 | ip="1.1.1.1",
32 | affiliate_id=self.user.id,
33 | offer_id=self.offer.id
34 | # id=x,
35 | # title='blabla',
36 | # description='blabla'
37 | ) for x in range(3)
38 | ]
39 |
40 | def test_list(self):
41 | response = self.client.get(self.list_url)
42 | self.assertEqual(200, response.status_code)
43 | self.assertIn('offer', response.data[0])
44 | self.assertEqual(self.offer.id, response.data[0]['offer']['id'])
45 | # self.assertIn('description', response.data[0])
46 | # self.assertIn('stop_at', response.data[0])
47 |
--------------------------------------------------------------------------------
/affiliate/tests/test_offer.py:
--------------------------------------------------------------------------------
1 | from rest_framework.test import APITestCase
2 | from django.contrib.auth import get_user_model
3 | from offer.models import Offer
4 |
5 |
6 | class OfferTestCase(APITestCase):
7 | list_url = '/affiliate/offers/'
8 | retrieve_url = '/affiliate/offers/1/'
9 | tracking_link_url = '/affiliate/offers/1/tracking-link/'
10 |
11 | def setUp(self):
12 | super(OfferTestCase, self).setUp()
13 | credentials = {
14 | 'username': 'test@test.com',
15 | 'password': '1234',
16 | }
17 | self.user = get_user_model().objects.create_user(**credentials)
18 | self.client.login(**credentials)
19 | self.offers = [
20 | Offer.objects.create(
21 | id=x,
22 | title='blabla',
23 | description='blabla'
24 | ) for x in range(3)
25 | ]
26 |
27 | def test_list(self):
28 | response = self.client.get(self.list_url)
29 | self.assertEqual(200, response.status_code)
30 | self.assertIn('title', response.data[0])
31 | self.assertIn('description', response.data[0])
32 | # self.assertIn('stop_at', response.data[0])
33 |
34 | def test_retrieve(self):
35 | response = self.client.get(self.retrieve_url)
36 | self.assertEqual(200, response.status_code)
37 | self.assertIn('title', response.data)
38 | self.assertIn('description', response.data)
39 | self.assertIn('preview_link', response.data)
40 | self.assertIn('countries', response.data)
41 | self.assertIn('categories', response.data)
42 | self.assertIn('traffic_sources', response.data)
43 | self.assertIn('payouts', response.data)
44 |
45 | def test_tracking_link(self):
46 | response = self.client.get(self.tracking_link_url)
47 | self.assertEqual(200, response.status_code)
48 | self.assertIn('url', response.data)
49 |
--------------------------------------------------------------------------------
/affiliate/tests/test_stats.py:
--------------------------------------------------------------------------------
1 | import uuid
2 | import pytz
3 | import datetime
4 | from rest_framework.test import APITestCase
5 | from django.contrib.auth import get_user_model
6 | from offer.models import Offer
7 | from tracker.models import Click, Conversion
8 |
9 |
10 | class StatsTestCase(APITestCase):
11 | offers_stats_url = '/affiliate/stats/offers/'
12 | goal_stats_url = '/affiliate/stats/by-goal/'
13 | daily_stats_url = '/affiliate/stats/daily/'
14 | sub_stats_url = '/affiliate/stats/by-sub/1/?offer_id=1'
15 |
16 | def setUp(self):
17 | super(StatsTestCase, self).setUp()
18 | credentials = {
19 | 'username': 'test@test.com',
20 | 'password': '1234',
21 | }
22 | self.user = get_user_model().objects.create_user(**credentials)
23 | self.client.login(**credentials)
24 | self.offer = Offer.objects.create(
25 | id=1,
26 | title='blabla',
27 | description='blabla'
28 | )
29 | sample_values = {
30 | 'affiliate_id': self.user.id,
31 | 'offer_id': self.offer.id,
32 | 'ip': '1.1.1.1',
33 | 'revenue': 0.0,
34 | 'payout': 0.0
35 | }
36 | for _ in range(100):
37 | Click.objects.create(**sample_values)
38 | for _ in range(3):
39 | Conversion.objects.create(
40 | click_id=uuid.uuid4(),
41 | click_date=datetime.datetime.now(pytz.UTC),
42 | **sample_values
43 | )
44 |
45 | def test_offers_stats(self):
46 | response = self.client.get(self.offers_stats_url)
47 | self.assertEqual(200, response.status_code)
48 | self.assertTrue(bool(len(response.data)))
49 | self.assertIn('offer_id', response.data[0])
50 | self.assertIn('offer_title', response.data[0])
51 | self.assertIn('clicks', response.data[0])
52 | self.assertTrue(response.data[0]['clicks'] == 100)
53 | self.assertIn('total_qty', response.data[0])
54 | self.assertIn('approved_qty', response.data[0])
55 | self.assertIn('hold_qty', response.data[0])
56 | self.assertIn('rejected_qty', response.data[0])
57 | self.assertIn('cr', response.data[0])
58 | self.assertIn('approved_payout', response.data[0])
59 | self.assertIn('hold_payout', response.data[0])
60 | self.assertIn('rejected_payout', response.data[0])
61 | self.assertIn('total_payout', response.data[0])
62 |
63 | def test_daily_stats(self):
64 | response = self.client.get(self.daily_stats_url)
65 | self.assertEqual(200, response.status_code)
66 | self.assertTrue(bool(len(response.data)))
67 | self.assertIn('date', response.data[0])
68 | self.assertIn('clicks', response.data[0])
69 | self.assertTrue(response.data[0]['clicks'] == 100)
70 | self.assertIn('total_qty', response.data[0])
71 | self.assertIn('approved_qty', response.data[0])
72 | self.assertIn('hold_qty', response.data[0])
73 | self.assertIn('rejected_qty', response.data[0])
74 | self.assertIn('cr', response.data[0])
75 | self.assertIn('approved_payout', response.data[0])
76 | self.assertIn('hold_payout', response.data[0])
77 | self.assertIn('rejected_payout', response.data[0])
78 | self.assertIn('total_payout', response.data[0])
79 |
80 | def test_goal_stats(self):
81 | response = self.client.get(self.goal_stats_url)
82 | self.assertEqual(200, response.status_code)
83 | self.assertIn('goal_id', response.data[0])
84 | self.assertIn('goal_name', response.data[0])
85 | self.assertIn('total_qty', response.data[0])
86 | self.assertIn('approved_qty', response.data[0])
87 | self.assertIn('hold_qty', response.data[0])
88 | self.assertIn('rejected_qty', response.data[0])
89 | self.assertIn('approved_payout', response.data[0])
90 | self.assertIn('hold_payout', response.data[0])
91 | self.assertIn('rejected_payout', response.data[0])
92 | self.assertIn('total_payout', response.data[0])
93 |
94 | def test_sub_stats(self):
95 | response = self.client.get(self.sub_stats_url)
96 | self.assertEqual(200, response.status_code)
97 | self.assertIn('sub', response.data[0])
98 | self.assertIn('clicks', response.data[0])
99 | self.assertIn('total_qty', response.data[0])
100 | self.assertIn('approved_qty', response.data[0])
101 | self.assertIn('hold_qty', response.data[0])
102 | self.assertIn('rejected_qty', response.data[0])
103 | self.assertIn('cr', response.data[0])
104 | self.assertIn('approved_payout', response.data[0])
105 | self.assertIn('hold_payout', response.data[0])
106 | self.assertIn('rejected_payout', response.data[0])
107 | self.assertIn('total_payout', response.data[0])
108 |
--------------------------------------------------------------------------------
/affiliate/urls.py:
--------------------------------------------------------------------------------
1 | from django.urls import path
2 |
3 | from .views.profile import AffiliateRetrieveAPIView
4 | from .views.register import CreateUserView
5 | from .views.offers import OfferListView, OfferRetrieveView, TrackingLinkView
6 | from .views.stats import (
7 | DailyStatsView,
8 | OffersStatsView,
9 | GoalStatsView,
10 | SubStatsView,
11 | )
12 | from .views.conversions import ConversionListView
13 |
14 |
15 | urlpatterns = [
16 | path(
17 | 'profile/',
18 | AffiliateRetrieveAPIView.as_view(), name='affiliate-profile'),
19 | path(
20 | 'sign-up/',
21 | CreateUserView.as_view(), name='affiliate-sign-up'),
22 | path(
23 | 'offers/', OfferListView.as_view(), name='affiliate-offers'),
24 | path(
25 | 'offers//',
26 | OfferRetrieveView.as_view(), name='affiliate-offer'),
27 | path(
28 | 'offers//tracking-link/',
29 | TrackingLinkView.as_view(), name='affiliate-offer-tracking-link'),
30 | path(
31 | 'stats/daily/',
32 | DailyStatsView.as_view(), name='affiliate-stats-daily'),
33 | path(
34 | 'stats/offers/',
35 | OffersStatsView.as_view(), name='affiliate-stats-offers'),
36 | path(
37 | 'conversions/',
38 | ConversionListView.as_view(), name='affiliate-conversions'),
39 | path(
40 | 'stats/by-goal/',
41 | GoalStatsView.as_view(), name='affiliate-stats-by-goal'),
42 | path(
43 | 'stats/by-sub//',
44 | SubStatsView.as_view(), name='affiliate-stats-by-sub'),
45 | ]
46 |
--------------------------------------------------------------------------------
/affiliate/views/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cpanova/cpa-network/1b5d1fbb3dfaa64441ee0be505b9eb75b44cb659/affiliate/views/__init__.py
--------------------------------------------------------------------------------
/affiliate/views/conversions.py:
--------------------------------------------------------------------------------
1 | import iso8601
2 | import pytz
3 | from datetime import datetime, time, date
4 | from rest_framework import serializers
5 | from rest_framework.views import APIView
6 | from rest_framework.response import Response
7 | from rest_framework.permissions import IsAuthenticated
8 | from tracker.models import Conversion
9 | from django.conf import settings
10 | from offer.models import Offer, Goal, Currency
11 |
12 |
13 | class OfferSerializer(serializers.ModelSerializer):
14 | class Meta:
15 | model = Offer
16 | fields = (
17 | 'id',
18 | 'title',
19 | )
20 |
21 |
22 | class GoalSerializer(serializers.ModelSerializer):
23 | class Meta:
24 | model = Goal
25 | fields = (
26 | 'name',
27 | )
28 |
29 |
30 | class CurrencySerializer(serializers.ModelSerializer):
31 | class Meta:
32 | model = Currency
33 | fields = (
34 | 'code',
35 | 'name',
36 | )
37 |
38 |
39 | class ConversionSerializer(serializers.ModelSerializer):
40 | offer = OfferSerializer()
41 | goal = GoalSerializer()
42 | currency = CurrencySerializer()
43 | id = serializers.SerializerMethodField()
44 | click_id = serializers.SerializerMethodField()
45 |
46 | def get_id(self, obj):
47 | return obj.id.hex
48 |
49 | def get_click_id(self, obj):
50 | if obj.click_id:
51 | return obj.click_id.hex
52 | else:
53 | return None
54 |
55 | class Meta:
56 | model = Conversion
57 | fields = (
58 | 'id',
59 | 'created_at',
60 | 'click_id',
61 | 'offer',
62 | 'payout',
63 | 'sub1',
64 | 'sub2',
65 | 'sub3',
66 | 'sub4',
67 | 'sub5',
68 | 'status',
69 | 'goal',
70 | 'currency',
71 | 'country',
72 | 'ip',
73 | 'ua',
74 | 'comment',
75 | )
76 |
77 |
78 | class ConversionListView(APIView):
79 | permission_classes = (IsAuthenticated,)
80 |
81 | def get(self, request):
82 | start_date_arg = request.query_params.get('start_date')
83 | end_date_arg = request.query_params.get('end_date')
84 | offer_id = request.query_params.get('offer_id')
85 |
86 | if start_date_arg:
87 | start_date = iso8601.parse_date(start_date_arg)
88 | else:
89 | start_date = date.today()
90 |
91 | if end_date_arg:
92 | end_date = iso8601.parse_date(end_date_arg)
93 | else:
94 | end_date = date.today()
95 |
96 | tz = pytz.timezone(settings.TIME_ZONE)
97 | start_datetime = tz.localize(datetime.combine(start_date, time.min))
98 | end_datetime = tz.localize(datetime.combine(end_date, time.max))
99 |
100 | filters = {
101 | 'created_at__range': [start_datetime, end_datetime],
102 | 'affiliate_id': request.user.id,
103 | }
104 |
105 | if offer_id:
106 | filters['offer_id'] = offer_id
107 |
108 | objs = (
109 | Conversion.objects
110 | .filter(**filters)
111 | .order_by('-created_at')
112 | )
113 |
114 | return Response(ConversionSerializer(objs, many=True).data)
115 |
--------------------------------------------------------------------------------
/affiliate/views/offers.py:
--------------------------------------------------------------------------------
1 | import django_filters
2 | from rest_framework import generics
3 | from rest_framework import serializers
4 | from rest_framework.views import APIView
5 | from rest_framework.response import Response
6 | from rest_framework.permissions import IsAuthenticated
7 | from django_filters.rest_framework import DjangoFilterBackend
8 | from rest_framework.filters import SearchFilter, OrderingFilter
9 | from countries_plus.models import Country
10 | from django.conf import settings
11 | from offer.models import (
12 | Offer,
13 | Category,
14 | Currency,
15 | Goal,
16 | TrafficSource,
17 | OfferTrafficSource,
18 | Payout
19 | )
20 | from ..filters import CommaSeparatedTextFilter
21 |
22 |
23 | class CountrySerializer(serializers.ModelSerializer):
24 | class Meta:
25 | model = Country
26 | fields = (
27 | 'iso',
28 | )
29 |
30 |
31 | class CategorySerializer(serializers.ModelSerializer):
32 | class Meta:
33 | model = Category
34 | fields = (
35 | 'name',
36 | )
37 |
38 |
39 | class TrafficSourceSerializer(serializers.ModelSerializer):
40 | class Meta:
41 | model = TrafficSource
42 | fields = (
43 | 'name',
44 | )
45 |
46 |
47 | class OfferTrafficSourceSerializer(serializers.ModelSerializer):
48 | name = serializers.SlugRelatedField(
49 | source='traffic_source',
50 | many=False, read_only=True, slug_field='name'
51 | )
52 |
53 | class Meta:
54 | model = OfferTrafficSource
55 | fields = (
56 | 'name',
57 | 'allowed'
58 | )
59 |
60 |
61 | class GoalSerializer(serializers.ModelSerializer):
62 | class Meta:
63 | model = Goal
64 | fields = (
65 | 'name',
66 | )
67 |
68 |
69 | class CurrencySerializer(serializers.ModelSerializer):
70 | class Meta:
71 | model = Currency
72 | fields = (
73 | 'code',
74 | 'name',
75 | )
76 |
77 |
78 | class PayoutSerializer(serializers.ModelSerializer):
79 | countries = CountrySerializer(many=True, read_only=True)
80 | goal = GoalSerializer(read_only=True)
81 | currency = CurrencySerializer(read_only=True)
82 |
83 | class Meta:
84 | model = Payout
85 | fields = (
86 | 'payout',
87 | 'countries',
88 | 'type',
89 | 'currency',
90 | 'goal',
91 | )
92 |
93 |
94 | class OfferSerializer(serializers.ModelSerializer):
95 | countries = CountrySerializer(many=True, read_only=True)
96 | categories = CategorySerializer(many=True, read_only=True)
97 | traffic_sources = OfferTrafficSourceSerializer(
98 | source='offertrafficsource_set', many=True, read_only=True)
99 | payouts = PayoutSerializer(many=True, read_only=True)
100 |
101 | class Meta:
102 | model = Offer
103 | fields = (
104 | 'id',
105 | 'title',
106 | 'description',
107 | 'description_html',
108 | 'preview_link',
109 | 'icon',
110 | 'countries',
111 | 'categories',
112 | 'traffic_sources',
113 | 'payouts',
114 | )
115 |
116 |
117 | class OfferFilterSet(django_filters.FilterSet):
118 | categories = CommaSeparatedTextFilter(
119 | field_name='categories',
120 | help_text='Exact category name or comma-separated names list'
121 | )
122 | countries = CommaSeparatedTextFilter(
123 | field_name='countries',
124 | help_text='Country 2-character code or comma-separated list'
125 | )
126 |
127 |
128 | class OfferListView(generics.ListAPIView):
129 | permission_classes = (IsAuthenticated,)
130 | serializer_class = OfferSerializer
131 | queryset = Offer.objects
132 | filter_backends = (DjangoFilterBackend, SearchFilter, OrderingFilter)
133 | filterset_class = OfferFilterSet
134 | search_fields = ['=id', 'title']
135 | ordering_fields = ['id', 'title']
136 |
137 |
138 | class OfferRetrieveView(generics.RetrieveAPIView):
139 | permission_classes = (IsAuthenticated,)
140 | serializer_class = OfferSerializer
141 | queryset = Offer.objects
142 |
143 |
144 | def generate_tracking_link(offer_id: int, pid: int) -> str:
145 | base_url = settings.TRACKER_URL
146 | url = f"{base_url}/click?offer_id={offer_id}&pid={pid}"
147 | return url
148 |
149 |
150 | class TrackingLinkView(APIView):
151 | permission_classes = (IsAuthenticated,)
152 |
153 | def get(self, request, pk):
154 | offer_id = pk
155 | user_id = request.user.id
156 | # offer = Offer.objects.get(pk=offer_id)
157 |
158 | url = generate_tracking_link(offer_id, user_id)
159 | return Response({'url': url})
160 |
161 | # if offer.access == ACCESS_TYPE_PUBLIC:
162 | # url = generate_tracking_link(offer_id, user_id)
163 | # return Response({'url': url})
164 |
165 | # if offer.access == ACCESS_TYPE_PREMODERATION:
166 | # approved = (
167 | # Approval.objects
168 | # .filter(
169 | # offer_id=offer_id,
170 | # affiliate_id=user_id,
171 | # status=APPROVAL_STATUS_APPROVED)
172 | # .exists())
173 | # if approved:
174 | # url = generate_tracking_link(offer_id, user_id)
175 | # return Response({'url': url})
176 | # else:
177 | # return Response(status=status.HTTP_403_FORBIDDEN)
178 |
--------------------------------------------------------------------------------
/affiliate/views/profile.py:
--------------------------------------------------------------------------------
1 | from rest_framework import serializers
2 | from rest_framework.response import Response
3 | from rest_framework.generics import RetrieveAPIView
4 | from rest_framework.permissions import IsAuthenticated
5 | # from ..models import Affiliate
6 | from django.contrib.auth import get_user_model
7 |
8 |
9 | class AffiliateModelSerializer(serializers.ModelSerializer):
10 | name = serializers.CharField(source='first_name')
11 |
12 | class Meta:
13 | model = get_user_model()
14 | fields = (
15 | 'name',
16 | )
17 |
18 |
19 | class AffiliateRetrieveAPIView(RetrieveAPIView):
20 | permission_classes = (IsAuthenticated,)
21 | serializer_class = AffiliateModelSerializer
22 |
23 | def get(self, request, *args, **kwargs):
24 | serializer = self.get_serializer(self.request.user.affiliate)
25 | return Response(serializer.data)
26 |
--------------------------------------------------------------------------------
/affiliate/views/register.py:
--------------------------------------------------------------------------------
1 | from rest_framework import serializers
2 | from rest_framework.generics import CreateAPIView
3 | from rest_framework.permissions import AllowAny
4 | from rest_framework.exceptions import ValidationError
5 | from django.contrib.auth import get_user_model
6 |
7 |
8 | def check_username_exists(username: str) -> bool:
9 | try:
10 | get_user_model().objects.get(username=username)
11 | except get_user_model().DoesNotExist:
12 | return False
13 | else:
14 | return True
15 |
16 |
17 | class UserSerializer(serializers.ModelSerializer):
18 | password = serializers.CharField(write_only=True)
19 | confirm_password = serializers.CharField(write_only=True)
20 |
21 | def create(self, validated_data):
22 | if validated_data["password"] != validated_data["confirm_password"]:
23 | raise ValidationError(
24 | {'confirm_password': ['Passwords do not match']}
25 | )
26 |
27 | if check_username_exists(validated_data['email']):
28 | raise ValidationError(
29 | {'email': [
30 | f"Email {validated_data['email']} is already registered"
31 | ]}
32 | )
33 |
34 | user = get_user_model().objects.create(
35 | email=validated_data['email'],
36 | username=validated_data['email']
37 | )
38 | user.set_password(validated_data['password'])
39 | user.is_staff = False
40 | user.is_active = True
41 | user.save()
42 |
43 | return user
44 |
45 | class Meta:
46 | model = get_user_model()
47 | fields = ('id', 'email', 'password', 'confirm_password',)
48 | write_only_fields = ('password',)
49 | read_only_fields = ('id',)
50 |
51 |
52 | class CreateUserView(CreateAPIView):
53 | model = get_user_model()
54 | permission_classes = (AllowAny,)
55 | serializer_class = UserSerializer
56 |
--------------------------------------------------------------------------------
/affiliate/views/stats.py:
--------------------------------------------------------------------------------
1 | import iso8601
2 | from datetime import datetime, time, timedelta, date
3 | from rest_framework import status
4 | from rest_framework.views import APIView
5 | from rest_framework.response import Response
6 | from rest_framework.permissions import IsAuthenticated
7 | from ..dao import daily_report, offer_report, goal_report, report_bysub
8 |
9 |
10 | class DailyStatsView(APIView):
11 | permission_classes = (IsAuthenticated,)
12 |
13 | def get(self, request):
14 | start_date_arg = request.query_params.get('start_date')
15 | end_date_arg = request.query_params.get('end_date')
16 | offer_id = request.query_params.get('offer_id')
17 |
18 | if start_date_arg:
19 | start_date = iso8601.parse_date(start_date_arg)
20 | else:
21 | start_date = date.today() - timedelta(days=6)
22 |
23 | if end_date_arg:
24 | end_date = iso8601.parse_date(end_date_arg)
25 | else:
26 | end_date = date.today()
27 |
28 | start_datetime = datetime.combine(start_date, time.min)
29 | end_datetime = datetime.combine(end_date, time.max)
30 |
31 | data = daily_report(
32 | request.user.id, start_datetime, end_datetime, offer_id)
33 |
34 | return Response(data)
35 |
36 |
37 | class OffersStatsView(APIView):
38 | permission_classes = (IsAuthenticated,)
39 |
40 | def get(self, request):
41 | start_date_arg = request.query_params.get('start_date')
42 | end_date_arg = request.query_params.get('end_date')
43 |
44 | if start_date_arg:
45 | start_date = iso8601.parse_date(start_date_arg)
46 | else:
47 | start_date = date.today() - timedelta(days=6)
48 |
49 | if end_date_arg:
50 | end_date = iso8601.parse_date(end_date_arg)
51 | else:
52 | end_date = date.today()
53 |
54 | start_datetime = datetime.combine(start_date, time.min)
55 | end_datetime = datetime.combine(end_date, time.max)
56 |
57 | data = offer_report(request.user.id, start_datetime, end_datetime)
58 |
59 | return Response(data)
60 |
61 |
62 | class GoalStatsView(APIView):
63 | permission_classes = (IsAuthenticated,)
64 |
65 | def get(self, request):
66 | start_date_arg = request.query_params.get('start_date')
67 | end_date_arg = request.query_params.get('end_date')
68 |
69 | if start_date_arg:
70 | start_date = iso8601.parse_date(start_date_arg)
71 | else:
72 | start_date = date.today() - timedelta(days=6)
73 |
74 | if end_date_arg:
75 | end_date = iso8601.parse_date(end_date_arg)
76 | else:
77 | end_date = date.today()
78 |
79 | start_datetime = datetime.combine(start_date, time.min)
80 | end_datetime = datetime.combine(end_date, time.max)
81 |
82 | data = goal_report(request.user.id, start_datetime, end_datetime)
83 |
84 | return Response(data)
85 |
86 |
87 | class SubStatsView(APIView):
88 | permission_classes = (IsAuthenticated,)
89 |
90 | def get(self, request, sub):
91 | if sub not in range(1, 6):
92 | return Response(status.HTTP_404_NOT_FOUND)
93 |
94 | offer_id = request.query_params.get('offer_id')
95 | if not offer_id:
96 | return Response(
97 | {'message': 'offer must be specified'},
98 | status.HTTP_400_BAD_REQUEST
99 | )
100 |
101 | start_date_arg = request.query_params.get('start_date')
102 | end_date_arg = request.query_params.get('end_date')
103 |
104 | if start_date_arg:
105 | start_date = iso8601.parse_date(start_date_arg)
106 | else:
107 | start_date = date.today() - timedelta(days=6)
108 |
109 | if end_date_arg:
110 | end_date = iso8601.parse_date(end_date_arg)
111 | else:
112 | end_date = date.today()
113 |
114 | start_datetime = datetime.combine(start_date, time.min)
115 | end_datetime = datetime.combine(end_date, time.max)
116 |
117 | data = report_bysub(
118 | sub, offer_id, request.user.id,
119 | start_datetime, end_datetime
120 | )
121 |
122 | return Response(data)
123 |
--------------------------------------------------------------------------------
/api/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cpanova/cpa-network/1b5d1fbb3dfaa64441ee0be505b9eb75b44cb659/api/__init__.py
--------------------------------------------------------------------------------
/api/permissions.py:
--------------------------------------------------------------------------------
1 | from rest_framework.permissions import BasePermission
2 |
3 |
4 | class IsSuperUser(BasePermission):
5 | """
6 | Allows access only to super users.
7 | """
8 |
9 | def has_permission(self, request, view):
10 | return bool(request.user and request.user.is_superuser)
11 |
--------------------------------------------------------------------------------
/api/tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cpanova/cpa-network/1b5d1fbb3dfaa64441ee0be505b9eb75b44cb659/api/tests/__init__.py
--------------------------------------------------------------------------------
/api/tests/test_advertiser.py:
--------------------------------------------------------------------------------
1 | from rest_framework.test import APITestCase
2 | from django.contrib.auth import get_user_model
3 |
4 |
5 | class AdvertiserTestCase(APITestCase):
6 | url = '/api/advertisers/'
7 |
8 | def setUp(self):
9 | super(AdvertiserTestCase, self).setUp()
10 | credentials = {
11 | 'username': 'test@test.com',
12 | 'password': '1234',
13 | }
14 | self.user = get_user_model().objects.create_user(
15 | id=1, is_staff=True, is_superuser=True, **credentials)
16 | self.client.login(**credentials)
17 |
18 | def test_create(self):
19 | data = {
20 | 'company': 'ROX',
21 | 'email': '...',
22 | 'comment': '...',
23 | }
24 | response = self.client.post(self.url, data, format='json')
25 | self.assertEqual(201, response.status_code)
26 |
27 | offer = response.json()
28 | self.assertEqual(offer['company'], "ROX")
29 |
--------------------------------------------------------------------------------
/api/tests/test_conversion.py:
--------------------------------------------------------------------------------
1 | from rest_framework.test import APITestCase
2 | from django.contrib.auth import get_user_model
3 | from offer.models import Offer, Currency, Goal
4 |
5 |
6 | class ConversionTestCase(APITestCase):
7 | url = '/api/conversions/'
8 |
9 | def setUp(self):
10 | super(ConversionTestCase, self).setUp()
11 | credentials = {
12 | 'username': 'test@test.com',
13 | 'password': '1234',
14 | }
15 | self.user = get_user_model().objects.create_user(
16 | id=1, is_staff=True, is_superuser=True, **credentials)
17 | self.client.login(**credentials)
18 | Offer.objects.create(id=1, title='a', description='a')
19 | Currency.objects.create(id=1, code="USD", name="Dollar")
20 | Goal.objects.create(id=6, name='Lead')
21 |
22 | def test_create(self):
23 | data = {
24 | "offer_id": 1,
25 | "pid": 1,
26 | "goal": '1',
27 | "goal_id": 6,
28 | "revenue": 32,
29 | "payout": 28,
30 | "currency": 'USD',
31 | "status": "hold",
32 | }
33 |
34 | response = self.client.post(self.url, data, format='json')
35 | self.assertEqual(201, response.status_code)
36 |
37 | conversion = response.json()
38 | self.assertEqual(conversion['offer_id'], 1)
39 | self.assertEqual(conversion['affiliate_id'], 1)
40 | self.assertEqual(conversion['goal_value'], '1')
41 | self.assertEqual(float(conversion['revenue']), 32)
42 | self.assertEqual(float(conversion['payout']), 28)
43 | self.assertEqual(conversion['currency']['code'], 'USD')
44 | self.assertEqual(conversion['status'], 'hold')
45 | self.assertEqual(conversion['goal']['name'], 'Lead')
46 |
--------------------------------------------------------------------------------
/api/tests/test_landing.py:
--------------------------------------------------------------------------------
1 | from rest_framework.test import APITestCase
2 | from django.contrib.auth import get_user_model
3 | from offer.models import Offer
4 |
5 |
6 | class LandingTestCase(APITestCase):
7 | url = '/api/landings/'
8 |
9 | def setUp(self):
10 | super(LandingTestCase, self).setUp()
11 | credentials = {
12 | 'username': 'test@test.com',
13 | 'password': '1234',
14 | }
15 | self.user = get_user_model().objects.create_user(
16 | id=1, is_staff=True, is_superuser=True, **credentials)
17 | self.client.login(**credentials)
18 | Offer.objects.create(id=1, title='a', description='a')
19 |
20 | def test_create(self):
21 | data = {
22 | 'offer_id': 1,
23 | 'name': 'reg',
24 | 'url': 'https://ya.ru',
25 | 'preview_url': '...',
26 | }
27 | response = self.client.post(self.url, data, format='json')
28 | self.assertEqual(201, response.status_code)
29 |
30 | landing = response.json()
31 | self.assertEqual(landing['name'], 'reg')
32 |
--------------------------------------------------------------------------------
/api/tests/test_offer.py:
--------------------------------------------------------------------------------
1 | from rest_framework.test import APITestCase
2 | from countries_plus.models import Country
3 | from django.contrib.auth import get_user_model
4 |
5 |
6 | class OfferTestCase(APITestCase):
7 | url = '/api/offers/'
8 |
9 | def setUp(self):
10 | super(OfferTestCase, self).setUp()
11 | credentials = {
12 | 'username': 'test@test.com',
13 | 'password': '1234',
14 | }
15 | self.user = get_user_model().objects.create_user(
16 | id=1, is_staff=True, is_superuser=True, **credentials)
17 | self.client.login(**credentials)
18 | Country.objects.create(
19 | iso='RU', iso3='RUS', iso_numeric=1, name='Russia'
20 | )
21 |
22 | def test_create(self):
23 | data = {
24 | "title": "test offer",
25 | 'countries': ['RU']
26 | }
27 | response = self.client.post(self.url, data, format='json')
28 | self.assertEqual(201, response.status_code)
29 |
30 | offer = response.json()
31 | self.assertEqual(offer['title'], "test offer")
32 |
--------------------------------------------------------------------------------
/api/tests/test_offer_traffic_source.py:
--------------------------------------------------------------------------------
1 | from rest_framework.test import APITestCase
2 | from django.contrib.auth import get_user_model
3 | from offer.models import Offer, TrafficSource
4 |
5 |
6 | class OfferTrafficSourceTestCase(APITestCase):
7 | url = '/api/traffic-sources/'
8 |
9 | def setUp(self):
10 | super(OfferTrafficSourceTestCase, self).setUp()
11 | credentials = {
12 | 'username': 'test@test.com',
13 | 'password': '1234',
14 | }
15 | self.user = get_user_model().objects.create_user(
16 | id=1, is_staff=True, is_superuser=True, **credentials)
17 | self.client.login(**credentials)
18 | Offer.objects.create(id=1, title='a', description='a')
19 | TrafficSource.objects.create(id=1, name='a')
20 |
21 | def test_create(self):
22 | data = {
23 | 'offer_id': 1,
24 | 'traffic_source_id': 1,
25 | 'allowed': False,
26 | }
27 | response = self.client.post(self.url, data, format='json')
28 | self.assertEqual(201, response.status_code)
29 |
30 | offer_traffic_source = response.json()
31 | self.assertEqual(offer_traffic_source['traffic_source']['id'], 1)
32 | self.assertEqual(offer_traffic_source['allowed'], False)
33 |
--------------------------------------------------------------------------------
/api/tests/test_payout.py:
--------------------------------------------------------------------------------
1 | from rest_framework.test import APITestCase
2 | from countries_plus.models import Country
3 | from django.contrib.auth import get_user_model
4 | from offer.models import Offer, Currency, Goal
5 |
6 |
7 | class PayoutTestCase(APITestCase):
8 | url = '/api/payouts/'
9 |
10 | def setUp(self):
11 | super(PayoutTestCase, self).setUp()
12 | credentials = {
13 | 'username': 'test@test.com',
14 | 'password': '1234',
15 | }
16 | self.user = get_user_model().objects.create_user(
17 | id=1, is_staff=True, is_superuser=True, **credentials)
18 | self.client.login(**credentials)
19 | Offer.objects.create(id=1, title='a', description='a')
20 | Currency.objects.create(id=1, code='USD', name='US Dollar')
21 | Goal.objects.create(id=1, name='Reg')
22 | Country.objects.create(
23 | iso='RU', iso3='RUS', iso_numeric=1, name='Russia'
24 | )
25 |
26 | def test_create(self):
27 | data = {
28 | 'offer_id': 1,
29 | 'currency_id': 1,
30 | 'goal_id': 1,
31 | 'revenue': '1.0',
32 | 'payout': '0.8',
33 | 'goal_value': '2',
34 | 'countries': ['RU']
35 | }
36 | response = self.client.post(self.url, data, format='json')
37 | self.assertEqual(201, response.status_code)
38 |
39 | payout = response.json()
40 | self.assertEqual(payout['goal_value'], '2')
41 | # self.assertEqual(payout['offer_id'], '1')
42 |
--------------------------------------------------------------------------------
/api/urls.py:
--------------------------------------------------------------------------------
1 | from django.urls import path, include
2 | from rest_framework.routers import DefaultRouter
3 | from .views.conversions import ConversionCreateView
4 | from .views.offer import OfferViewSet
5 | from .views.payout import PayoutViewSet
6 | from .views.landing import LandingViewSet
7 | from .views.offer_traffic_source import OfferTrafficSourceViewSet
8 | from .views.advertiser import AdvertiserViewSet
9 |
10 |
11 | router = DefaultRouter()
12 | router.register(r'offers', OfferViewSet, basename="offer")
13 | router.register(r'advertisers', AdvertiserViewSet, basename="advertiser")
14 | router.register(r'landings', LandingViewSet, basename="landing")
15 | router.register(r'payouts', PayoutViewSet, basename="payout")
16 | router.register(
17 | r'traffic-sources',
18 | OfferTrafficSourceViewSet, basename="traffic-source"
19 | )
20 |
21 |
22 | urlpatterns = [
23 | path('', include(router.urls)),
24 | path(
25 | 'conversions/',
26 | ConversionCreateView.as_view(), name='api-conversions'),
27 | ]
28 |
--------------------------------------------------------------------------------
/api/views/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cpanova/cpa-network/1b5d1fbb3dfaa64441ee0be505b9eb75b44cb659/api/views/__init__.py
--------------------------------------------------------------------------------
/api/views/advertiser.py:
--------------------------------------------------------------------------------
1 | from rest_framework import serializers
2 | from rest_framework.viewsets import ModelViewSet
3 | from rest_framework.permissions import IsAuthenticated
4 | from ..permissions import IsSuperUser
5 | from offer.models import Advertiser
6 |
7 |
8 | class AdvertiserSerializer(serializers.ModelSerializer):
9 | class Meta:
10 | model = Advertiser
11 | fields = '__all__'
12 | depth = 1
13 |
14 |
15 | class AdvertiserViewSet(ModelViewSet):
16 | permission_classes = (IsAuthenticated, IsSuperUser,)
17 | serializer_class = AdvertiserSerializer
18 | queryset = Advertiser.objects
19 |
--------------------------------------------------------------------------------
/api/views/conversions.py:
--------------------------------------------------------------------------------
1 | import rest_framework.status
2 | from rest_framework import serializers
3 | from rest_framework.views import APIView
4 | from rest_framework.response import Response
5 | from rest_framework.permissions import IsAuthenticated
6 | from webargs import core, fields, ValidationError
7 | from webargs.djangoparser import parser
8 | from django.contrib.auth import get_user_model
9 | from tracker.models import (
10 | Conversion, conversion_statuses, REJECTED_STATUS
11 | )
12 | from ..permissions import IsSuperUser
13 | from offer.models import Currency, Goal
14 |
15 |
16 | class CurrencySerializer(serializers.ModelSerializer):
17 | class Meta:
18 | model = Currency
19 | fields = (
20 | 'code',
21 | 'name',
22 | )
23 |
24 |
25 | class GoalSerializer(serializers.ModelSerializer):
26 | class Meta:
27 | model = Goal
28 | fields = (
29 | 'id',
30 | 'name',
31 | )
32 |
33 |
34 | class ConversionSerializer(serializers.ModelSerializer):
35 | currency = CurrencySerializer()
36 | goal = GoalSerializer()
37 |
38 | class Meta:
39 | model = Conversion
40 | fields = (
41 | 'id', # TODO .hex
42 | 'created_at',
43 | 'offer_id',
44 | 'affiliate_id',
45 | # TODO offer.name
46 | 'revenue',
47 | 'payout',
48 | 'currency',
49 | 'sub1',
50 | 'sub2',
51 | 'sub3',
52 | 'sub4',
53 | 'sub5',
54 | 'status',
55 | 'goal',
56 | 'goal_value',
57 | 'country',
58 | 'ip',
59 | 'ua',
60 | )
61 |
62 |
63 | @parser.location_loader('data')
64 | def parse_data(request, name, field):
65 | return core.get_value(request.data, name, field)
66 |
67 |
68 | def user_must_exist_in_db(user_id: int) -> None:
69 | try:
70 | get_user_model().objects.get(pk=user_id)
71 | except get_user_model().DoesNotExist:
72 | raise ValidationError("Affiliate does not exist")
73 |
74 |
75 | def status_must_be_known(status: str) -> None:
76 | if status and status not in map(lambda r: r[0], conversion_statuses):
77 | raise ValidationError("Wrong status value")
78 |
79 |
80 | conversion_create_args = {
81 | 'offer_id': fields.Int(required=True),
82 | 'pid': fields.Int(required=True, validate=user_must_exist_in_db),
83 | 'status': fields.Str(
84 | missing=REJECTED_STATUS,
85 | validate=status_must_be_known),
86 | 'currency': fields.Str(missing=''),
87 | 'goal': fields.Str(missing=''),
88 | 'revenue': fields.Float(missing=.0),
89 | 'payout': fields.Float(missing=.0),
90 | 'sub1': fields.Str(missing=''),
91 | 'goal_id': fields.Int(missing=None),
92 | }
93 |
94 |
95 | class ConversionCreateView(APIView):
96 | permission_classes = (IsAuthenticated, IsSuperUser,)
97 |
98 | @parser.use_args(conversion_create_args)
99 | def post(self, request, args):
100 | usr = get_user_model().objects.get(pk=args['pid'])
101 |
102 | conversion = Conversion()
103 | conversion.offer_id = args['offer_id']
104 | conversion.affiliate_id = args['pid']
105 | conversion.affiliate_manager = usr.profile.manager
106 | conversion.goal_value = args['goal']
107 | conversion.revenue = args['revenue']
108 | conversion.payout = args['payout']
109 | conversion.sub1 = args['sub1']
110 | conversion.currency = (
111 | Currency.objects.filter(code=args['currency']).first())
112 | conversion.status = args['status']
113 | if args['goal_id']:
114 | conversion.goal_id = args['goal_id']
115 | conversion.save()
116 |
117 | return Response(
118 | ConversionSerializer(conversion).data,
119 | status=rest_framework.status.HTTP_201_CREATED
120 | )
121 |
--------------------------------------------------------------------------------
/api/views/landing.py:
--------------------------------------------------------------------------------
1 | from rest_framework import serializers
2 | from rest_framework.viewsets import ModelViewSet
3 | from rest_framework.permissions import IsAuthenticated
4 | from ..permissions import IsSuperUser
5 | from offer.models import Landing
6 |
7 |
8 | class LandingSerializer(serializers.ModelSerializer):
9 | offer_id = serializers.IntegerField(write_only=True)
10 |
11 | class Meta:
12 | model = Landing
13 | fields = '__all__'
14 | depth = 1
15 |
16 |
17 | class LandingViewSet(ModelViewSet):
18 | permission_classes = (IsAuthenticated, IsSuperUser,)
19 | serializer_class = LandingSerializer
20 | queryset = Landing.objects
21 | filterset_fields = ['offer_id']
22 |
--------------------------------------------------------------------------------
/api/views/offer.py:
--------------------------------------------------------------------------------
1 | from rest_framework import serializers
2 | from rest_framework.viewsets import ModelViewSet
3 | from rest_framework.permissions import IsAuthenticated
4 | from ..permissions import IsSuperUser
5 | from offer.models import Offer
6 |
7 |
8 | class OfferSerializer(serializers.ModelSerializer):
9 | class Meta:
10 | model = Offer
11 | fields = '__all__'
12 | depth = 1
13 |
14 |
15 | class OfferViewSet(ModelViewSet):
16 | permission_classes = (IsAuthenticated, IsSuperUser,)
17 | serializer_class = OfferSerializer
18 | queryset = Offer.objects
19 |
--------------------------------------------------------------------------------
/api/views/offer_traffic_source.py:
--------------------------------------------------------------------------------
1 | from rest_framework import serializers
2 | from rest_framework.viewsets import ModelViewSet
3 | from rest_framework.permissions import IsAuthenticated
4 | from ..permissions import IsSuperUser
5 | from offer.models import OfferTrafficSource
6 |
7 |
8 | class OfferTrafficSourceSerializer(serializers.ModelSerializer):
9 | offer_id = serializers.IntegerField(write_only=True)
10 | traffic_source_id = serializers.IntegerField(write_only=True)
11 |
12 | class Meta:
13 | model = OfferTrafficSource
14 | fields = '__all__'
15 | depth = 1
16 |
17 |
18 | class OfferTrafficSourceViewSet(ModelViewSet):
19 | permission_classes = (IsAuthenticated, IsSuperUser,)
20 | serializer_class = OfferTrafficSourceSerializer
21 | queryset = OfferTrafficSource.objects
22 | filterset_fields = ['offer_id']
23 |
--------------------------------------------------------------------------------
/api/views/payout.py:
--------------------------------------------------------------------------------
1 | from rest_framework import serializers
2 | from rest_framework.viewsets import ModelViewSet
3 | from rest_framework.permissions import IsAuthenticated
4 | from ..permissions import IsSuperUser
5 | from offer.models import Payout
6 |
7 |
8 | class PayoutSerializer(serializers.ModelSerializer):
9 | offer_id = serializers.IntegerField(write_only=True)
10 | currency_id = serializers.IntegerField(write_only=True)
11 |
12 | class Meta:
13 | model = Payout
14 | fields = '__all__'
15 | depth = 1
16 |
17 |
18 | class PayoutViewSet(ModelViewSet):
19 | permission_classes = (IsAuthenticated, IsSuperUser,)
20 | serializer_class = PayoutSerializer
21 | queryset = Payout.objects
22 | filterset_fields = ['offer_id']
23 |
--------------------------------------------------------------------------------
/app.json:
--------------------------------------------------------------------------------
1 | {
2 | "scripts": {
3 | "dokku": {
4 | "postdeploy": "python manage.py migrate"
5 | }
6 | }
7 | }
--------------------------------------------------------------------------------
/dictionaries/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cpanova/cpa-network/1b5d1fbb3dfaa64441ee0be505b9eb75b44cb659/dictionaries/__init__.py
--------------------------------------------------------------------------------
/dictionaries/apps.py:
--------------------------------------------------------------------------------
1 | from django.apps import AppConfig
2 |
3 |
4 | class DictionariesConfig(AppConfig):
5 | name = 'dictionaries'
6 |
--------------------------------------------------------------------------------
/dictionaries/urls.py:
--------------------------------------------------------------------------------
1 | from django.urls import path
2 | from .views import countries, categories
3 |
4 |
5 | urlpatterns = [
6 | path('countries/', countries.CountryListView.as_view(), name='countries'),
7 | path(
8 | 'categories/',
9 | categories.CategoryListView.as_view(), name='categories'),
10 | ]
11 |
--------------------------------------------------------------------------------
/dictionaries/views/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cpanova/cpa-network/1b5d1fbb3dfaa64441ee0be505b9eb75b44cb659/dictionaries/views/__init__.py
--------------------------------------------------------------------------------
/dictionaries/views/categories.py:
--------------------------------------------------------------------------------
1 | from rest_framework import generics
2 | from rest_framework import serializers
3 | from rest_framework import permissions
4 |
5 | from offer.models import Category
6 |
7 |
8 | class CategorySerializer(serializers.ModelSerializer):
9 | class Meta:
10 | model = Category
11 | fields = (
12 | 'id',
13 | 'name',
14 | )
15 |
16 |
17 | class CategoryListView(generics.ListAPIView):
18 | permission_classes = (permissions.AllowAny,)
19 | serializer_class = CategorySerializer
20 | queryset = Category.objects
21 |
--------------------------------------------------------------------------------
/dictionaries/views/countries.py:
--------------------------------------------------------------------------------
1 | from rest_framework import generics
2 | from rest_framework import serializers
3 | from rest_framework import permissions
4 |
5 | from countries_plus.models import Country
6 |
7 |
8 | class CountrySerializer(serializers.ModelSerializer):
9 | class Meta:
10 | model = Country
11 | fields = (
12 | 'iso',
13 | 'name',
14 | )
15 |
16 |
17 | class CountryListView(generics.ListAPIView):
18 | permission_classes = (permissions.AllowAny,)
19 | serializer_class = CountrySerializer
20 | queryset = Country.objects
21 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '2'
2 |
3 |
4 | services:
5 |
6 | web:
7 | build:
8 | context: .
9 | dockerfile: ./docker/Dockerfile
10 | image: sandbox/affiliate-platform
11 | command: honcho start --procfile Procfile.dev
12 | volumes:
13 | - .:/app
14 | ports:
15 | - 8000:80
16 | environment:
17 | PYTHONUNBUFFERED: 1
18 | env_file:
19 | - .env
20 | depends_on:
21 | - postgres
22 | - redis
23 |
24 | postgres:
25 | image: "postgres:12-alpine"
26 | environment:
27 | - POSTGRES_DB=postgres
28 | - POSTGRES_USER=postgres
29 | - POSTGRES_PASSWORD=postgres
30 | volumes:
31 | - ./volumes/postgres:/var/lib/postgresql/data
32 |
33 | redis:
34 | image: redis:alpine
35 |
--------------------------------------------------------------------------------
/docker/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM python:3
2 |
3 | RUN pip install --upgrade pip
4 |
5 | WORKDIR /app
6 |
7 | COPY requirements.dev.txt .
8 |
9 | RUN pip install -r requirements.dev.txt
10 |
11 | COPY . .
12 |
13 | ENV PYTHONPATH /app
14 |
15 | EXPOSE 80
16 |
17 | CMD ["python", "manage.py", "runserver", "0.0.0.0:80"]
18 |
--------------------------------------------------------------------------------
/docs/api-spec.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cpanova/cpa-network/1b5d1fbb3dfaa64441ee0be505b9eb75b44cb659/docs/api-spec.png
--------------------------------------------------------------------------------
/docs/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Swagger
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
23 |
24 |
25 |
38 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/ext/ipapi/__init__.py:
--------------------------------------------------------------------------------
1 | from .api import (
2 | API,
3 | Err,
4 | TimeoutErr
5 | )
6 |
7 |
8 | __all__ = [
9 | 'API',
10 | 'Err',
11 | 'TimeoutErr',
12 | ]
13 |
--------------------------------------------------------------------------------
/ext/ipapi/api.py:
--------------------------------------------------------------------------------
1 | import requests
2 | from typing import NamedTuple
3 |
4 |
5 | BASE_URL = "http://ip-api.com/json"
6 |
7 |
8 | class Err(Exception): pass # noqa
9 | class TimeoutErr(Err): pass # noqa
10 |
11 |
12 | class Response(NamedTuple):
13 | country_code: str
14 |
15 |
16 | class API(object):
17 |
18 | def query(self, ip: str) -> Response:
19 | url = f'{BASE_URL}/{ip}'
20 | params = {
21 | 'fields': 'countryCode',
22 | }
23 | try:
24 | resp = requests.get(url, params=params)
25 | except requests.Timeout:
26 | raise TimeoutErr()
27 | if resp.status_code != 200:
28 | raise Err()
29 | data = resp.json()
30 | return Response(
31 | country_code=(data.get('countryCode', '') or ''),
32 | )
33 |
--------------------------------------------------------------------------------
/ext/ipstack/__init__.py:
--------------------------------------------------------------------------------
1 | from .api import (
2 | API,
3 | Err,
4 | TimeoutErr
5 | )
6 |
7 |
8 | __all__ = [
9 | 'API',
10 | 'Err',
11 | 'TimeoutErr',
12 | ]
13 |
--------------------------------------------------------------------------------
/ext/ipstack/api.py:
--------------------------------------------------------------------------------
1 | import requests
2 | from typing import NamedTuple
3 |
4 |
5 | BASE_URL = "http://api.ipstack.com/"
6 |
7 |
8 | class Err(Exception): pass # noqa
9 | class TimeoutErr(Err): pass # noqa
10 |
11 |
12 | class Response(NamedTuple):
13 | ip: str
14 | country_code: str
15 |
16 |
17 | class API(object):
18 |
19 | def __init__(self, token):
20 | self.token = token
21 |
22 | def lookup(self, ip: str) -> Response:
23 | url = f'{BASE_URL}/{ip}'
24 | params = {
25 | 'access_key': self.token,
26 | }
27 | try:
28 | resp = requests.get(url, params=params)
29 | except requests.Timeout:
30 | raise TimeoutErr()
31 | if resp.status_code != 200:
32 | raise Err()
33 | data = resp.json()
34 | return Response(
35 | ip=data['ip'],
36 | country_code=data['country_code'] or '',
37 | )
38 |
--------------------------------------------------------------------------------
/manage.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | """Django's command-line utility for administrative tasks."""
3 | import os
4 | import sys
5 |
6 |
7 | def main():
8 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'project.settings')
9 | try:
10 | from django.core.management import execute_from_command_line
11 | except ImportError as exc:
12 | raise ImportError(
13 | "Couldn't import Django. Are you sure it's installed and "
14 | "available on your PYTHONPATH environment variable? Did you "
15 | "forget to activate a virtual environment?"
16 | ) from exc
17 | execute_from_command_line(sys.argv)
18 |
19 |
20 | if __name__ == '__main__':
21 | main()
22 |
--------------------------------------------------------------------------------
/mypy.ini:
--------------------------------------------------------------------------------
1 | [mypy]
2 | # Mypy configuration:
3 | # https://mypy.readthedocs.io/en/latest/config_file.html
4 | allow_redefinition = False
5 | check_untyped_defs = True
6 | disallow_untyped_decorators = True
7 | disallow_any_explicit = True
8 | disallow_any_generics = True
9 | disallow_untyped_calls = True
10 | ignore_errors = False
11 | ignore_missing_imports = True
12 | implicit_reexport = False
13 | local_partial_types = True
14 | strict_optional = True
15 | strict_equality = True
16 | no_implicit_optional = True
17 | warn_unused_ignores = True
18 | warn_redundant_casts = True
19 | warn_unused_configs = True
20 | warn_unreachable = True
21 | warn_no_return = True
22 |
23 | plugins =
24 | mypy_django_plugin.main,
25 | mypy_drf_plugin.main
26 |
27 | [mypy.plugins.django-stubs]
28 | django_settings_module = "project.settings"
29 |
30 | [mypy-server.apps.*.migrations.*]
31 | # Django migrations should not produce any errors:
32 | ignore_errors = True
33 |
34 | [mypy-server.apps.*.models]
35 | # FIXME: remove this line, when `django-stubs` will stop
36 | # using `Any` inside.
37 | disallow_any_explicit = False
--------------------------------------------------------------------------------
/network/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cpanova/cpa-network/1b5d1fbb3dfaa64441ee0be505b9eb75b44cb659/network/__init__.py
--------------------------------------------------------------------------------
/network/apps.py:
--------------------------------------------------------------------------------
1 | from django.apps import AppConfig
2 |
3 |
4 | class NetworkConfig(AppConfig):
5 | name = 'network'
6 |
--------------------------------------------------------------------------------
/network/dao.py:
--------------------------------------------------------------------------------
1 | import os
2 | from typing import List, Dict
3 | from datetime import datetime
4 | from django.db import connection
5 |
6 |
7 | def daily_report(
8 | user_id: int, start_date: datetime, end_date: datetime,
9 | offer_id: int = 0) -> List[Dict]:
10 |
11 | if offer_id:
12 | offer_filter_clause = f"AND offer_id = {offer_id}"
13 | else:
14 | offer_filter_clause = ''
15 |
16 | filepath = os.path.join(
17 | os.path.dirname(__file__), 'sql', 'daily_report.sql'
18 | )
19 | with open(filepath, 'r') as f:
20 | sql = f.read().format(**locals())
21 |
22 | colnames = [
23 | 'day',
24 |
25 | 'clicks',
26 |
27 | 'approved_qty',
28 | 'approved_revenue',
29 | 'hold_qty',
30 | 'hold_revenue',
31 | 'rejected_qty',
32 | 'rejected_revenue',
33 |
34 | 'cr',
35 |
36 | 'total_qty',
37 | 'total_revenue',
38 | 'total_payout',
39 | 'total_profit',
40 | ]
41 |
42 | with connection.cursor() as cursor:
43 | cursor.execute(sql)
44 | data = cursor.fetchall()
45 | data = [dict(zip(colnames, row)) for row in data]
46 |
47 | return data
48 |
49 |
50 | def offer_report(
51 | user_id: int, start_date: datetime, end_date: datetime) -> List[Dict]:
52 |
53 | colnames = [
54 | 'offer_id',
55 | 'offer_title',
56 |
57 | 'clicks',
58 |
59 | 'approved_qty',
60 | 'approved_revenue',
61 | 'hold_qty',
62 | 'hold_revenue',
63 | 'rejected_qty',
64 | 'rejected_revenue',
65 |
66 | 'cr',
67 |
68 | 'total_qty',
69 | 'total_revenue',
70 | 'total_payout',
71 | 'total_profit',
72 | ]
73 |
74 | filepath = os.path.join(
75 | os.path.dirname(__file__), 'sql', 'offer_report.sql'
76 | )
77 | with open(filepath, 'r') as f:
78 | sql = f.read().format(**locals())
79 |
80 | with connection.cursor() as cursor:
81 | cursor.execute(sql)
82 | data = cursor.fetchall()
83 | data = [dict(zip(colnames, row)) for row in data]
84 |
85 | return data
86 |
87 |
88 | def affiliate_report(
89 | user_id: int, start_date: datetime, end_date: datetime) -> List[Dict]:
90 |
91 | colnames = [
92 | 'affiliate_id',
93 | 'affiliate_name',
94 |
95 | 'clicks',
96 |
97 | 'approved_qty',
98 | 'approved_revenue',
99 | 'hold_qty',
100 | 'hold_revenue',
101 | 'rejected_qty',
102 | 'rejected_revenue',
103 |
104 | 'cr',
105 |
106 | 'total_qty',
107 | 'total_revenue',
108 | 'total_payout',
109 | 'total_profit',
110 | ]
111 |
112 | filepath = os.path.join(
113 | os.path.dirname(__file__), 'sql', 'affiliate_report.sql'
114 | )
115 | with open(filepath, 'r') as f:
116 | sql = f.read().format(**locals())
117 |
118 | with connection.cursor() as cursor:
119 | cursor.execute(sql)
120 | data = cursor.fetchall()
121 | data = [dict(zip(colnames, row)) for row in data]
122 |
123 | return data
124 |
--------------------------------------------------------------------------------
/network/migrations/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cpanova/cpa-network/1b5d1fbb3dfaa64441ee0be505b9eb75b44cb659/network/migrations/__init__.py
--------------------------------------------------------------------------------
/network/sql/affiliate_report.sql:
--------------------------------------------------------------------------------
1 | SELECT
2 | report.affiliate_id,
3 | u.email,
4 | COALESCE(report.clicks, 0),
5 |
6 | COALESCE(report.approved_qty, 0),
7 | COALESCE(report.approved_revenue, 0),
8 | COALESCE(report.hold_qty, 0),
9 | COALESCE(report.hold_revenue, 0),
10 | COALESCE(report.rejected_qty, 0),
11 | COALESCE(report.rejected_revenue, 0),
12 |
13 | COALESCE(report.cr, 0),
14 |
15 | COALESCE(report.total_qty, 0),
16 | COALESCE(report.total_revenue, 0),
17 | COALESCE(report.total_payout, 0),
18 | COALESCE(report.total_profit, 0)
19 | FROM
20 | (
21 | SELECT
22 | COALESCE(cl.affiliate_id, cv.affiliate_id) as affiliate_id,
23 | cl.clicks,
24 | cv.approved_qty,
25 | cv.approved_revenue,
26 | cv.hold_qty,
27 | cv.hold_revenue,
28 | cv.rejected_qty,
29 | cv.rejected_revenue,
30 | case cl.clicks
31 | when 0 then 0 -- avoid divizion by zero
32 | else (100 * cv.total_qty / cl.clicks)
33 | end AS cr,
34 | cv.total_qty,
35 | cv.total_revenue,
36 | cv.total_payout,
37 | (cv.total_revenue - cv.total_payout) as total_profit
38 | FROM
39 | (
40 | SELECT
41 | affiliate_id,
42 | count(*) as clicks
43 | FROM tracker_click
44 | WHERE
45 | affiliate_manager_id = {user_id}
46 | AND created_at between '{start_date}' AND '{end_date}'
47 | GROUP BY affiliate_id
48 | ) AS cl
49 | FULL OUTER JOIN
50 | (
51 | SELECT
52 | affiliate_id,
53 | count(*) AS total_qty,
54 | sum(payout) AS total_payout,
55 | sum(revenue) AS total_revenue,
56 | count(*) FILTER (WHERE status = 'approved') AS approved_qty,
57 | sum(revenue) FILTER (WHERE status = 'approved') AS approved_revenue,
58 | count(*) FILTER (WHERE status = 'hold') AS hold_qty,
59 | sum(revenue) FILTER (WHERE status = 'hold') AS hold_revenue,
60 | count(*) FILTER (WHERE status = 'rejected') AS rejected_qty,
61 | sum(revenue) FILTER (WHERE status = 'rejected') AS rejected_revenue
62 | FROM tracker_conversion
63 | WHERE
64 | affiliate_manager_id = {user_id}
65 | AND created_at between '{start_date}' AND '{end_date}'
66 | GROUP BY affiliate_id
67 | ) AS cv
68 | ON cl.affiliate_id = cv.affiliate_id
69 | ) AS report
70 | LEFT JOIN auth_user AS u
71 | ON report.affiliate_id = u.id
72 | ;
--------------------------------------------------------------------------------
/network/sql/daily_report.sql:
--------------------------------------------------------------------------------
1 | SELECT
2 | COALESCE(cl.day, cv.day),
3 | COALESCE(cl.clicks, 0),
4 | COALESCE(cv.approved_qty, 0),
5 | COALESCE(cv.approved_revenue, 0),
6 | COALESCE(cv.hold_qty, 0),
7 | COALESCE(cv.hold_revenue, 0),
8 | COALESCE(cv.rejected_qty, 0),
9 | COALESCE(cv.rejected_revenue, 0),
10 | COALESCE(
11 | case cl.clicks
12 | when 0 then 0 -- avoid divizion by zero
13 | else (100 * cv.total_qty / cl.clicks)
14 | end
15 | , 0) AS cr,
16 | COALESCE(cv.total_qty, 0),
17 | COALESCE(cv.total_revenue, 0),
18 | COALESCE(cv.total_payout, 0),
19 | COALESCE(cv.total_revenue - cv.total_payout, 0) as total_profit
20 | FROM
21 | (
22 | SELECT
23 | created_at::date AS day,
24 | count(*) AS clicks
25 | FROM tracker_click
26 | WHERE
27 | affiliate_manager_id = {user_id}
28 | AND created_at between '{start_date}' AND '{end_date}'
29 | {offer_filter_clause}
30 | GROUP BY day
31 | ) AS cl
32 | FULL OUTER JOIN
33 | (
34 | SELECT
35 | created_at::date AS day,
36 | count(*) AS total_qty,
37 | sum(payout) AS total_payout,
38 | sum(revenue) AS total_revenue,
39 | count(*) FILTER (WHERE status = 'approved') AS approved_qty,
40 | sum(revenue) FILTER (WHERE status = 'approved') AS approved_revenue,
41 | count(*) FILTER (WHERE status = 'hold') AS hold_qty,
42 | sum(revenue) FILTER (WHERE status = 'hold') AS hold_revenue,
43 | count(*) FILTER (WHERE status = 'rejected') AS rejected_qty,
44 | sum(revenue) FILTER (WHERE status = 'rejected') AS rejected_revenue
45 | FROM tracker_conversion
46 | WHERE
47 | affiliate_manager_id = {user_id}
48 | AND created_at between '{start_date}' AND '{end_date}'
49 | {offer_filter_clause}
50 | GROUP BY day
51 | ) AS cv
52 | ON cl.day = cv.day
53 | ORDER BY cl.day DESC
54 | ;
--------------------------------------------------------------------------------
/network/sql/offer_report.sql:
--------------------------------------------------------------------------------
1 | SELECT
2 | report.offer_id,
3 | o.title,
4 | COALESCE(report.clicks, 0),
5 |
6 | COALESCE(report.approved_qty, 0),
7 | COALESCE(report.approved_revenue, 0),
8 | COALESCE(report.hold_qty, 0),
9 | COALESCE(report.hold_revenue, 0),
10 | COALESCE(report.rejected_qty, 0),
11 | COALESCE(report.rejected_revenue, 0),
12 |
13 | COALESCE(report.cr, 0),
14 |
15 | COALESCE(report.total_qty, 0),
16 | COALESCE(report.total_revenue, 0),
17 | COALESCE(report.total_payout, 0),
18 | COALESCE(report.total_profit, 0)
19 |
20 | FROM
21 | (
22 | SELECT
23 | COALESCE(cl.offer_id, cv.offer_id) as offer_id,
24 | cl.clicks,
25 | cv.approved_qty,
26 | cv.approved_revenue,
27 | cv.hold_qty,
28 | cv.hold_revenue,
29 | cv.rejected_qty,
30 | cv.rejected_revenue,
31 | case cl.clicks
32 | when 0 then 0 -- avoid divizion by zero
33 | else (100 * cv.total_qty / cl.clicks)
34 | end AS cr,
35 | cv.total_qty,
36 | cv.total_revenue,
37 | cv.total_payout,
38 | cv.total_revenue - cv.total_payout as total_profit
39 | FROM
40 | (
41 | SELECT
42 | offer_id,
43 | count(*) as clicks
44 | FROM tracker_click
45 | WHERE
46 | affiliate_manager_id = {user_id}
47 | AND created_at between '{start_date}' AND '{end_date}'
48 | GROUP BY offer_id
49 | ) AS cl
50 | FULL OUTER JOIN
51 | (
52 | SELECT
53 | offer_id,
54 | count(*) AS total_qty,
55 | sum(payout) AS total_payout,
56 | sum(revenue) AS total_revenue,
57 | count(*) FILTER (WHERE status = 'approved') AS approved_qty,
58 | sum(revenue) FILTER (WHERE status = 'approved') AS approved_revenue,
59 | count(*) FILTER (WHERE status = 'hold') AS hold_qty,
60 | sum(revenue) FILTER (WHERE status = 'hold') AS hold_revenue,
61 | count(*) FILTER (WHERE status = 'rejected') AS rejected_qty,
62 | sum(revenue) FILTER (WHERE status = 'rejected') AS rejected_revenue
63 | FROM tracker_conversion
64 | WHERE
65 | affiliate_manager_id = {user_id}
66 | AND created_at between '{start_date}' AND '{end_date}'
67 | GROUP BY offer_id
68 | ) AS cv
69 | ON cl.offer_id = cv.offer_id
70 | ) AS report
71 | LEFT JOIN offer_offer AS o
72 | ON report.offer_id = o.id
73 | ;
--------------------------------------------------------------------------------
/network/tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cpanova/cpa-network/1b5d1fbb3dfaa64441ee0be505b9eb75b44cb659/network/tests/__init__.py
--------------------------------------------------------------------------------
/network/tests/test_affiliate.py:
--------------------------------------------------------------------------------
1 | from rest_framework.test import APITestCase
2 | from django.contrib.auth import get_user_model
3 |
4 |
5 | class AffiliateTestCase(APITestCase):
6 | list_url = '/network/affiliates/'
7 | retrieve_url = '/network/affiliates/{}/'
8 |
9 | def get_retrieve_url(self, id_):
10 | return self.retrieve_url.format(id_)
11 |
12 | def setUp(self):
13 | super(AffiliateTestCase, self).setUp()
14 | self.credentials = {
15 | 'username': 'test@test.com',
16 | 'password': '1234',
17 | }
18 | self.user = get_user_model().objects.create_user(**self.credentials)
19 | self.staff_credentials = {
20 | 'username': 'staff',
21 | 'password': '1234',
22 | }
23 | self.staff = (
24 | get_user_model().objects
25 | .create_user(is_staff=True, **self.staff_credentials)
26 | )
27 |
28 | def test_not_staff_cant_access_list(self):
29 | self.client.login(**self.credentials)
30 | response = self.client.get(self.list_url)
31 | self.assertEqual(403, response.status_code)
32 |
33 | def test_staff_can_access_list(self):
34 | self.client.login(**self.staff_credentials)
35 | response = self.client.get(self.list_url)
36 | self.assertEqual(200, response.status_code)
37 | self.assertIn('username', response.data[0])
38 | self.assertIn('email', response.data[0])
39 |
40 | def test_retrieve(self):
41 | self.client.login(**self.staff_credentials)
42 | response = self.client.get(self.get_retrieve_url(self.user.id))
43 | self.assertEqual(200, response.status_code)
44 | self.assertIn('username', response.data)
45 | self.assertIn('email', response.data)
46 |
--------------------------------------------------------------------------------
/network/tests/test_conversion.py:
--------------------------------------------------------------------------------
1 | import uuid
2 | import pytz
3 | import datetime
4 | from rest_framework.test import APITestCase
5 | from django.contrib.auth import get_user_model
6 | from offer.models import Offer
7 | from tracker.models import Conversion
8 |
9 |
10 | class ConversionTestCase(APITestCase):
11 | list_url = '/network/conversions/'
12 |
13 | def setUp(self):
14 | super(ConversionTestCase, self).setUp()
15 | credentials = {
16 | 'username': 'test@test.com',
17 | 'password': '1234',
18 | }
19 | self.user = get_user_model().objects.create_user(
20 | is_staff=True, **credentials)
21 | self.client.login(**credentials)
22 | self.offer = Offer.objects.create(
23 | title='blabla',
24 | description='blabla'
25 | )
26 | for x in range(3):
27 | Conversion.objects.create(
28 | click_id=uuid.uuid4(),
29 | click_date=datetime.datetime.now(pytz.UTC),
30 | revenue=0.0,
31 | payout=0.0,
32 | ip="1.1.1.1",
33 | affiliate_id=self.user.id,
34 | offer_id=self.offer.id
35 | )
36 |
37 | def test_list(self):
38 | response = self.client.get(self.list_url)
39 | self.assertEqual(200, response.status_code)
40 | self.assertIn('offer_id', response.data[0])
41 | self.assertEqual(self.offer.id, response.data[0]['offer_id'])
42 | # self.assertIn('description', response.data[0])
43 | # self.assertIn('stop_at', response.data[0])
44 |
--------------------------------------------------------------------------------
/network/tests/test_offer.py:
--------------------------------------------------------------------------------
1 | from rest_framework.test import APITestCase
2 | from django.contrib.auth import get_user_model
3 | from offer.models import Offer, Advertiser
4 |
5 |
6 | class OfferTestCase(APITestCase):
7 | list_url = '/network/offers/'
8 | retrieve_url = '/network/offers/1/'
9 |
10 | def setUp(self):
11 | super(OfferTestCase, self).setUp()
12 | credentials = {
13 | 'username': 'staff',
14 | 'password': '1234',
15 | }
16 | self.user = (
17 | get_user_model().objects
18 | .create_user(is_staff=True, **credentials)
19 | )
20 | self.client.login(**credentials)
21 | self.offers = [
22 | Offer.objects.create(
23 | id=offer_id,
24 | title='blabla',
25 | description='blabla',
26 | advertiser=(
27 | Advertiser.objects
28 | .create(company='x', email='x', comment='x')
29 | )
30 | ) for offer_id in range(3)
31 | ]
32 |
33 | def test_list(self):
34 | response = self.client.get(self.list_url)
35 | self.assertEqual(200, response.status_code)
36 | self.assertIn('title', response.data[0])
37 | self.assertIn('description', response.data[0])
38 |
39 | def test_retrieve(self):
40 | response = self.client.get(self.retrieve_url)
41 | self.assertEqual(200, response.status_code)
42 | self.assertIn('title', response.data)
43 | self.assertIn('description', response.data)
44 | self.assertIn('tracking_link', response.data)
45 | self.assertIn('preview_link', response.data)
46 | self.assertIn('countries', response.data)
47 | self.assertIn('status', response.data)
48 | self.assertIn('advertiser', response.data)
49 | self.assertIn('company', response.data['advertiser'])
50 | self.assertIn('categories', response.data)
51 | self.assertIn('traffic_sources', response.data)
52 | self.assertIn('payouts', response.data)
53 |
--------------------------------------------------------------------------------
/network/tests/test_stats.py:
--------------------------------------------------------------------------------
1 | import uuid
2 | import pytz
3 | import datetime
4 | from rest_framework.test import APITestCase
5 | from django.contrib.auth import get_user_model
6 | from offer.models import Offer
7 | from tracker.models import Click, Conversion
8 |
9 |
10 | class StatsTestCase(APITestCase):
11 | offers_stats_url = '/network/stats/offers/'
12 | daily_stats_url = '/network/stats/daily/'
13 | affiliates_stats_url = '/network/stats/affiliates/'
14 |
15 | def setUp(self):
16 | super(StatsTestCase, self).setUp()
17 | credentials = {
18 | 'username': 'affiliate',
19 | 'password': '1234',
20 | }
21 | self.affiliate = get_user_model().objects.create_user(**credentials)
22 | credentials = {
23 | 'username': 'staff',
24 | 'password': '1234',
25 | }
26 | self.manager = (
27 | get_user_model().objects
28 | .create_user(is_staff=True, **credentials)
29 | )
30 | self.client.login(**credentials)
31 | self.offer = Offer.objects.create(
32 | title='blabla',
33 | description='blabla'
34 | )
35 | sample_values = {
36 | 'affiliate_id': self.affiliate.id,
37 | 'affiliate_manager_id': self.manager.id,
38 | 'offer_id': self.offer.id,
39 | 'ip': '1.1.1.1',
40 | 'revenue': 0.0,
41 | 'payout': 0.0
42 | }
43 | for _ in range(100):
44 | Click.objects.create(**sample_values)
45 | for _ in range(3):
46 | Conversion.objects.create(
47 | click_id=uuid.uuid4(),
48 | click_date=datetime.datetime.now(pytz.UTC),
49 | **sample_values
50 | )
51 |
52 | def test_offers_stats(self):
53 | response = self.client.get(self.offers_stats_url)
54 | self.assertEqual(200, response.status_code)
55 | self.assertTrue(bool(len(response.data)))
56 | self.assertIn('clicks', response.data[0])
57 | self.assertTrue(response.data[0]['clicks'] == 100)
58 | self.assertIn('approved_qty', response.data[0])
59 | self.assertIn('approved_revenue', response.data[0])
60 | self.assertIn('hold_qty', response.data[0])
61 | self.assertIn('hold_revenue', response.data[0])
62 | self.assertIn('rejected_qty', response.data[0])
63 | self.assertIn('rejected_revenue', response.data[0])
64 | self.assertIn('cr', response.data[0])
65 | self.assertIn('total_qty', response.data[0])
66 | self.assertIn('total_revenue', response.data[0])
67 | self.assertIn('total_payout', response.data[0])
68 | self.assertIn('total_profit', response.data[0])
69 |
70 | def test_daily_stats(self):
71 | response = self.client.get(self.daily_stats_url)
72 | self.assertEqual(200, response.status_code)
73 | self.assertTrue(bool(len(response.data)))
74 | self.assertIn('clicks', response.data[0])
75 | self.assertTrue(response.data[0]['clicks'] == 100)
76 | self.assertIn('approved_qty', response.data[0])
77 | self.assertIn('approved_revenue', response.data[0])
78 | self.assertIn('hold_qty', response.data[0])
79 | self.assertIn('hold_revenue', response.data[0])
80 | self.assertIn('rejected_qty', response.data[0])
81 | self.assertIn('rejected_revenue', response.data[0])
82 | self.assertIn('cr', response.data[0])
83 | self.assertIn('total_qty', response.data[0])
84 | self.assertIn('total_revenue', response.data[0])
85 | self.assertIn('total_payout', response.data[0])
86 | self.assertIn('total_profit', response.data[0])
87 |
88 | def test_affiliates_stats(self):
89 | response = self.client.get(self.affiliates_stats_url)
90 | self.assertEqual(200, response.status_code)
91 | self.assertTrue(bool(len(response.data)))
92 | self.assertIn('affiliate_id', response.data[0])
93 | self.assertEqual(self.affiliate.id, response.data[0]['affiliate_id'])
94 | self.assertIn('clicks', response.data[0])
95 | self.assertTrue(response.data[0]['clicks'] == 100)
96 | self.assertIn('approved_qty', response.data[0])
97 | self.assertIn('approved_revenue', response.data[0])
98 | self.assertIn('hold_qty', response.data[0])
99 | self.assertIn('hold_revenue', response.data[0])
100 | self.assertIn('rejected_qty', response.data[0])
101 | self.assertIn('rejected_revenue', response.data[0])
102 | self.assertIn('cr', response.data[0])
103 | self.assertIn('total_qty', response.data[0])
104 | self.assertIn('total_revenue', response.data[0])
105 | self.assertIn('total_payout', response.data[0])
106 | self.assertIn('total_profit', response.data[0])
107 |
--------------------------------------------------------------------------------
/network/urls.py:
--------------------------------------------------------------------------------
1 | from django.urls import path
2 | from .views.affiliates import AffiliateListView, AffiliateRetrieveView
3 | from .views.offers import OfferListView, OfferRetrieveView
4 | from .views.stats import DailyStatsView, OffersStatsView, AffiliatesStatsView
5 | from .views.conversions import ConversionListView
6 |
7 |
8 | urlpatterns = [
9 | path(
10 | 'affiliates/',
11 | AffiliateListView.as_view(), name='network-affiliates'),
12 | path(
13 | 'affiliates//',
14 | AffiliateRetrieveView.as_view(), name='network-affiliate'),
15 | path('offers/', OfferListView.as_view(), name='network-offers'),
16 | path(
17 | 'offers//',
18 | OfferRetrieveView.as_view(), name='network-offer'),
19 | path(
20 | 'stats/daily/',
21 | DailyStatsView.as_view(), name='network-stats-daily'),
22 | path(
23 | 'stats/offers/',
24 | OffersStatsView.as_view(), name='network-stats-offers'),
25 | path(
26 | 'stats/affiliates/',
27 | AffiliatesStatsView.as_view(), name='network-stats-affiliates'),
28 | path(
29 | 'conversions/',
30 | ConversionListView.as_view(), name='network-conversions'),
31 | ]
32 |
--------------------------------------------------------------------------------
/network/views/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cpanova/cpa-network/1b5d1fbb3dfaa64441ee0be505b9eb75b44cb659/network/views/__init__.py
--------------------------------------------------------------------------------
/network/views/affiliates.py:
--------------------------------------------------------------------------------
1 | from rest_framework import generics
2 | from rest_framework import serializers
3 | from rest_framework.permissions import IsAuthenticated, IsAdminUser
4 | from django.contrib.auth import get_user_model
5 |
6 |
7 | class AffiliateSerializer(serializers.ModelSerializer):
8 | class Meta:
9 | model = get_user_model()
10 | fields = (
11 | 'id',
12 | 'username',
13 | 'email',
14 | 'first_name',
15 | 'last_name',
16 | )
17 |
18 |
19 | class AffiliateListView(generics.ListAPIView):
20 | permission_classes = (IsAuthenticated, IsAdminUser,)
21 | serializer_class = AffiliateSerializer
22 | # queryset = get_user_model().objects
23 |
24 | def get_queryset(self):
25 | return get_user_model().objects.filter(is_staff=False)
26 |
27 |
28 | class AffiliateRetrieveView(generics.RetrieveAPIView):
29 | permission_classes = (IsAuthenticated, IsAdminUser,)
30 | serializer_class = AffiliateSerializer
31 |
32 | def get_queryset(self):
33 | return get_user_model().objects.filter(is_staff=False)
34 |
--------------------------------------------------------------------------------
/network/views/conversions.py:
--------------------------------------------------------------------------------
1 | import iso8601
2 | import pytz
3 | from datetime import datetime, time, date
4 | from rest_framework import serializers
5 | from rest_framework.views import APIView
6 | from rest_framework.response import Response
7 | from rest_framework.permissions import IsAuthenticated, IsAdminUser
8 | from tracker.models import Conversion
9 | from django.conf import settings
10 |
11 |
12 | class ConversionSerializer(serializers.ModelSerializer):
13 |
14 | class Meta:
15 | model = Conversion
16 | fields = (
17 | 'id', # TODO .hex
18 | 'created_at',
19 | 'offer_id',
20 | # TODO offer.name
21 | 'revenue',
22 | 'payout',
23 | 'sub1',
24 | 'sub2',
25 | 'sub3',
26 | 'sub4',
27 | 'sub5',
28 | 'status',
29 | 'goal',
30 | 'goal_value',
31 | 'country',
32 | 'ip',
33 | 'ua',
34 | )
35 |
36 |
37 | class ConversionListView(APIView):
38 | permission_classes = (IsAuthenticated, IsAdminUser,)
39 |
40 | def get(self, request):
41 | start_date_arg = request.query_params.get('start_date')
42 | end_date_arg = request.query_params.get('end_date')
43 | offer_id = request.query_params.get('offer_id')
44 |
45 | if start_date_arg:
46 | start_date = iso8601.parse_date(start_date_arg)
47 | else:
48 | start_date = date.today()
49 |
50 | if end_date_arg:
51 | end_date = iso8601.parse_date(end_date_arg)
52 | else:
53 | end_date = date.today()
54 |
55 | tz = pytz.timezone(settings.TIME_ZONE)
56 | start_datetime = tz.localize(datetime.combine(start_date, time.min))
57 | end_datetime = tz.localize(datetime.combine(end_date, time.max))
58 |
59 | filters = {
60 | 'created_at__range': [start_datetime, end_datetime],
61 | 'affiliate_id': request.user.id,
62 | }
63 |
64 | if offer_id:
65 | filters['offer_id'] = offer_id
66 |
67 | objs = (
68 | Conversion.objects
69 | .filter(**filters)
70 | .order_by('-created_at')
71 | )
72 |
73 | return Response(ConversionSerializer(objs, many=True).data)
74 |
--------------------------------------------------------------------------------
/network/views/offers.py:
--------------------------------------------------------------------------------
1 | from rest_framework import generics
2 | from rest_framework import serializers
3 | from rest_framework.permissions import IsAuthenticated, IsAdminUser
4 | from countries_plus.models import Country
5 | from offer.models import (
6 | Offer,
7 | Category,
8 | Advertiser,
9 | Currency,
10 | Goal,
11 | TrafficSource,
12 | OfferTrafficSource,
13 | Payout
14 | )
15 |
16 |
17 | class CountrySerializer(serializers.ModelSerializer):
18 | class Meta:
19 | model = Country
20 | fields = (
21 | 'iso',
22 | )
23 |
24 |
25 | class GoalSerializer(serializers.ModelSerializer):
26 | class Meta:
27 | model = Goal
28 | fields = (
29 | 'name',
30 | )
31 |
32 |
33 | class CurrencySerializer(serializers.ModelSerializer):
34 | class Meta:
35 | model = Currency
36 | fields = (
37 | 'code',
38 | 'name',
39 | )
40 |
41 |
42 | class PayoutSerializer(serializers.ModelSerializer):
43 | countries = CountrySerializer(many=True, read_only=True)
44 | goal = GoalSerializer(read_only=True)
45 | currency = CurrencySerializer(read_only=True)
46 |
47 | class Meta:
48 | model = Payout
49 | fields = (
50 | 'revenue',
51 | 'payout',
52 | 'countries',
53 | 'goal_value',
54 | 'type',
55 | 'currency',
56 | 'goal'
57 | )
58 |
59 |
60 | class CategorySerializer(serializers.ModelSerializer):
61 | class Meta:
62 | model = Category
63 | fields = (
64 | 'name',
65 | )
66 |
67 |
68 | class TrafficSourceSerializer(serializers.ModelSerializer):
69 | class Meta:
70 | model = TrafficSource
71 | fields = (
72 | 'name',
73 | )
74 |
75 |
76 | class OfferTrafficSourceSerializer(serializers.ModelSerializer):
77 | name = serializers.SlugRelatedField(
78 | source='traffic_source',
79 | many=False, read_only=True, slug_field='name')
80 |
81 | class Meta:
82 | model = OfferTrafficSource
83 | fields = (
84 | 'name',
85 | 'allowed'
86 | )
87 |
88 |
89 | class AdvertiserSerializer(serializers.ModelSerializer):
90 | class Meta:
91 | model = Advertiser
92 | fields = (
93 | 'company',
94 | 'email',
95 | 'contact_person',
96 | 'messenger',
97 | 'site',
98 | 'comment',
99 | )
100 |
101 |
102 | class OfferSerializer(serializers.ModelSerializer):
103 | countries = CountrySerializer(many=True, read_only=True)
104 | categories = CategorySerializer(many=True, read_only=True)
105 | traffic_sources = OfferTrafficSourceSerializer(
106 | source='offertrafficsource_set', many=True, read_only=True)
107 | advertiser = AdvertiserSerializer(read_only=True)
108 | payouts = PayoutSerializer(many=True, read_only=True)
109 |
110 | class Meta:
111 | model = Offer
112 | fields = (
113 | 'id',
114 | 'title',
115 | 'description',
116 | 'tracking_link',
117 | 'preview_link',
118 | 'countries',
119 | 'categories',
120 | 'traffic_sources',
121 | 'status',
122 | 'advertiser',
123 | 'payouts',
124 | )
125 |
126 |
127 | class OfferListView(generics.ListAPIView):
128 | permission_classes = (IsAuthenticated, IsAdminUser,)
129 | serializer_class = OfferSerializer
130 | queryset = Offer.objects
131 |
132 |
133 | class OfferRetrieveView(generics.RetrieveAPIView):
134 | permission_classes = (IsAuthenticated, IsAdminUser,)
135 | serializer_class = OfferSerializer
136 | queryset = Offer.objects
137 |
--------------------------------------------------------------------------------
/network/views/stats.py:
--------------------------------------------------------------------------------
1 | import iso8601
2 | from datetime import datetime, time, timedelta, date
3 | from rest_framework.views import APIView
4 | from rest_framework.response import Response
5 | from rest_framework.permissions import IsAuthenticated, IsAdminUser
6 | from ..dao import daily_report, offer_report, affiliate_report
7 |
8 |
9 | class DailyStatsView(APIView):
10 | permission_classes = (IsAuthenticated, IsAdminUser,)
11 |
12 | def get(self, request):
13 | start_date_arg = request.query_params.get('start_date')
14 | end_date_arg = request.query_params.get('end_date')
15 | offer_id = request.query_params.get('offer_id')
16 |
17 | if start_date_arg:
18 | start_date = iso8601.parse_date(start_date_arg)
19 | else:
20 | start_date = date.today() - timedelta(days=6)
21 |
22 | if end_date_arg:
23 | end_date = iso8601.parse_date(end_date_arg)
24 | else:
25 | end_date = date.today()
26 |
27 | start_datetime = datetime.combine(start_date, time.min)
28 | end_datetime = datetime.combine(end_date, time.max)
29 |
30 | data = daily_report(
31 | request.user.id, start_datetime, end_datetime, offer_id)
32 |
33 | return Response(data)
34 |
35 |
36 | class OffersStatsView(APIView):
37 | permission_classes = (IsAuthenticated, IsAdminUser,)
38 |
39 | def get(self, request):
40 | start_date_arg = request.query_params.get('start_date')
41 | end_date_arg = request.query_params.get('end_date')
42 |
43 | if start_date_arg:
44 | start_date = iso8601.parse_date(start_date_arg)
45 | else:
46 | start_date = date.today() - timedelta(days=6)
47 |
48 | if end_date_arg:
49 | end_date = iso8601.parse_date(end_date_arg)
50 | else:
51 | end_date = date.today()
52 |
53 | start_datetime = datetime.combine(start_date, time.min)
54 | end_datetime = datetime.combine(end_date, time.max)
55 |
56 | data = offer_report(request.user.id, start_datetime, end_datetime)
57 |
58 | return Response(data)
59 |
60 |
61 | class AffiliatesStatsView(APIView):
62 | permission_classes = (IsAuthenticated, IsAdminUser,)
63 |
64 | def get(self, request):
65 | start_date_arg = request.query_params.get('start_date')
66 | end_date_arg = request.query_params.get('end_date')
67 |
68 | if start_date_arg:
69 | start_date = iso8601.parse_date(start_date_arg)
70 | else:
71 | start_date = date.today() - timedelta(days=6)
72 |
73 | if end_date_arg:
74 | end_date = iso8601.parse_date(end_date_arg)
75 | else:
76 | end_date = date.today()
77 |
78 | start_datetime = datetime.combine(start_date, time.min)
79 | end_datetime = datetime.combine(end_date, time.max)
80 |
81 | data = affiliate_report(request.user.id, start_datetime, end_datetime)
82 |
83 | return Response(data)
84 |
--------------------------------------------------------------------------------
/offer/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cpanova/cpa-network/1b5d1fbb3dfaa64441ee0be505b9eb75b44cb659/offer/__init__.py
--------------------------------------------------------------------------------
/offer/admin.py:
--------------------------------------------------------------------------------
1 | from django.contrib import admin
2 | from .models import (
3 | Offer,
4 | Category,
5 | TrafficSource,
6 | OfferTrafficSource,
7 | Goal,
8 | Currency,
9 | Payout,
10 | Landing,
11 | Advertiser,
12 | STOPPED_STATUS,
13 | )
14 |
15 |
16 | class OfferTrafficSource_inline(admin.TabularInline):
17 | model = OfferTrafficSource
18 | extra = 1
19 |
20 |
21 | class Payout_inline(admin.TabularInline):
22 | model = Payout
23 | extra = 1
24 |
25 |
26 | class Landing_inline(admin.TabularInline):
27 | model = Landing
28 | extra = 1
29 |
30 |
31 | def duplicate_offer(modeladmin, request, queryset):
32 | for offer in queryset:
33 | source_payouts = [p for p in offer.payouts.all()]
34 | source_countries = [c for c in offer.countries.all()]
35 | source_categories = [c for c in offer.categories.all()]
36 | source_ots = [ots for ots in offer.offertrafficsource_set.all()]
37 |
38 | offer.id = None
39 | offer.title = offer.title + ' DUPLICATE'
40 | offer.status = STOPPED_STATUS
41 | offer.save()
42 |
43 | for country in source_countries:
44 | offer.countries.add(country)
45 |
46 | for category in source_categories:
47 | offer.categories.add(category)
48 |
49 | for payout in source_payouts:
50 | source_payout_countries = [c for c in payout.countries.all()]
51 |
52 | payout.id = None
53 | payout.save()
54 |
55 | for country in source_payout_countries:
56 | payout.countries.add(country)
57 |
58 | offer.payouts.add(payout)
59 |
60 | for ots in source_ots:
61 | ots.id = None
62 | ots.save()
63 | offer.offertrafficsource_set.add(ots)
64 | # queryset.update(status=PAUSED_STATUS)
65 |
66 |
67 | duplicate_offer.short_description = "Duplicate"
68 |
69 |
70 | @admin.register(Offer)
71 | class OfferAdmin(admin.ModelAdmin):
72 | inlines = (
73 | OfferTrafficSource_inline,
74 | Payout_inline,
75 | Landing_inline,
76 | )
77 | list_display = (
78 | 'title', 'id', 'status', 'advertiser',
79 | )
80 | actions = [duplicate_offer]
81 |
82 |
83 | @admin.register(Category)
84 | class CategoryAdmin(admin.ModelAdmin):
85 | pass
86 |
87 |
88 | @admin.register(TrafficSource)
89 | class TrafficSourceAdmin(admin.ModelAdmin):
90 | pass
91 |
92 |
93 | @admin.register(Goal)
94 | class GoalAdmin(admin.ModelAdmin):
95 | pass
96 |
97 |
98 | @admin.register(Payout)
99 | class PayoutAdmin(admin.ModelAdmin):
100 | list_display = (
101 | 'offer', 'revenue', 'payout', 'currency',
102 | 'goal_value', 'goal',
103 | )
104 |
105 |
106 | @admin.register(Currency)
107 | class CurrencyAdmin(admin.ModelAdmin):
108 | pass
109 |
110 |
111 | @admin.register(Advertiser)
112 | class AdvertiserAdmin(admin.ModelAdmin):
113 | pass
114 |
--------------------------------------------------------------------------------
/offer/apps.py:
--------------------------------------------------------------------------------
1 | from django.apps import AppConfig
2 |
3 |
4 | class OfferConfig(AppConfig):
5 | name = 'offer'
6 |
--------------------------------------------------------------------------------
/offer/migrations/0001_initial.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 2.2.5 on 2019-09-21 20:33
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | initial = True
9 |
10 | dependencies = [
11 | ]
12 |
13 | operations = [
14 | migrations.CreateModel(
15 | name='Offer',
16 | fields=[
17 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
18 | ('title', models.CharField(max_length=256)),
19 | ],
20 | ),
21 | ]
22 |
--------------------------------------------------------------------------------
/offer/migrations/0002_offer_countries.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 2.2.5 on 2019-09-21 21:16
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('countries_plus', '0005_auto_20160224_1804'),
10 | ('offer', '0001_initial'),
11 | ]
12 |
13 | operations = [
14 | migrations.AddField(
15 | model_name='offer',
16 | name='countries',
17 | field=models.ManyToManyField(to='countries_plus.Country'),
18 | ),
19 | ]
20 |
--------------------------------------------------------------------------------
/offer/migrations/0003_offer_description.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 2.2.5 on 2019-09-24 20:40
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('offer', '0002_offer_countries'),
10 | ]
11 |
12 | operations = [
13 | migrations.AddField(
14 | model_name='offer',
15 | name='description',
16 | field=models.TextField(default=''),
17 | ),
18 | ]
19 |
--------------------------------------------------------------------------------
/offer/migrations/0004_auto_20191009_1712.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 2.2.5 on 2019-10-09 17:12
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('offer', '0003_offer_description'),
10 | ]
11 |
12 | operations = [
13 | migrations.AlterField(
14 | model_name='offer',
15 | name='title',
16 | field=models.CharField(default='', max_length=256),
17 | ),
18 | ]
19 |
--------------------------------------------------------------------------------
/offer/migrations/0005_category.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 2.2.6 on 2020-06-18 16:44
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('offer', '0004_auto_20191009_1712'),
10 | ]
11 |
12 | operations = [
13 | migrations.CreateModel(
14 | name='Category',
15 | fields=[
16 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
17 | ('name', models.CharField(max_length=256)),
18 | ],
19 | ),
20 | ]
21 |
--------------------------------------------------------------------------------
/offer/migrations/0006_trafficsource.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 2.2.6 on 2020-06-18 16:44
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('offer', '0005_category'),
10 | ]
11 |
12 | operations = [
13 | migrations.CreateModel(
14 | name='TrafficSource',
15 | fields=[
16 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
17 | ('name', models.CharField(max_length=256)),
18 | ],
19 | ),
20 | ]
21 |
--------------------------------------------------------------------------------
/offer/migrations/0007_offer_categories.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 2.2.6 on 2020-06-18 16:46
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('offer', '0006_trafficsource'),
10 | ]
11 |
12 | operations = [
13 | migrations.AddField(
14 | model_name='offer',
15 | name='categories',
16 | field=models.ManyToManyField(to='offer.Category'),
17 | ),
18 | ]
19 |
--------------------------------------------------------------------------------
/offer/migrations/0008_auto_20200618_1647.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 2.2.6 on 2020-06-18 16:47
2 |
3 | from django.db import migrations, models
4 | import django.db.models.deletion
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ('offer', '0007_offer_categories'),
11 | ]
12 |
13 | operations = [
14 | migrations.CreateModel(
15 | name='OfferTrafficSource',
16 | fields=[
17 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
18 | ('allowed', models.BooleanField(default=True)),
19 | ('offer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='offer.Offer')),
20 | ('traffic_source', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='offer.TrafficSource')),
21 | ],
22 | ),
23 | migrations.AddField(
24 | model_name='offer',
25 | name='traffic_sources',
26 | field=models.ManyToManyField(through='offer.OfferTrafficSource', to='offer.TrafficSource'),
27 | ),
28 | ]
29 |
--------------------------------------------------------------------------------
/offer/migrations/0009_auto_20200618_1706.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 2.2.6 on 2020-06-18 17:06
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('offer', '0008_auto_20200618_1647'),
10 | ]
11 |
12 | operations = [
13 | migrations.CreateModel(
14 | name='Goal',
15 | fields=[
16 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
17 | ('name', models.CharField(max_length=64)),
18 | ],
19 | ),
20 | migrations.AlterField(
21 | model_name='offer',
22 | name='categories',
23 | field=models.ManyToManyField(blank=True, to='offer.Category'),
24 | ),
25 | migrations.AlterField(
26 | model_name='offer',
27 | name='traffic_sources',
28 | field=models.ManyToManyField(blank=True, through='offer.OfferTrafficSource', to='offer.TrafficSource'),
29 | ),
30 | ]
31 |
--------------------------------------------------------------------------------
/offer/migrations/0010_currency.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 2.2.6 on 2020-06-18 17:15
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('offer', '0009_auto_20200618_1706'),
10 | ]
11 |
12 | operations = [
13 | migrations.CreateModel(
14 | name='Currency',
15 | fields=[
16 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
17 | ('code', models.CharField(max_length=3)),
18 | ('name', models.CharField(max_length=128)),
19 | ],
20 | ),
21 | ]
22 |
--------------------------------------------------------------------------------
/offer/migrations/0011_payout.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 2.2.6 on 2020-06-18 17:16
2 |
3 | from django.db import migrations, models
4 | import django.db.models.deletion
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ('countries_plus', '0005_auto_20160224_1804'),
11 | ('offer', '0010_currency'),
12 | ]
13 |
14 | operations = [
15 | migrations.CreateModel(
16 | name='Payout',
17 | fields=[
18 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
19 | ('revenue', models.DecimalField(decimal_places=2, max_digits=7)),
20 | ('payout', models.DecimalField(decimal_places=2, max_digits=7)),
21 | ('goal_value', models.CharField(default='1', max_length=20)),
22 | ('type', models.CharField(choices=[('Fixed', 'Fixed'), ('Percent', 'Percent')], default='Fixed', max_length=20)),
23 | ('countries', models.ManyToManyField(to='countries_plus.Country')),
24 | ('currency', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='offer.Currency')),
25 | ('goal', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='offer.Goal')),
26 | ('offer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='payouts', to='offer.Offer')),
27 | ],
28 | ),
29 | ]
30 |
--------------------------------------------------------------------------------
/offer/migrations/0012_auto_20200618_1922.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 2.2.6 on 2020-06-18 19:22
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('offer', '0011_payout'),
10 | ]
11 |
12 | operations = [
13 | migrations.AddField(
14 | model_name='offer',
15 | name='status',
16 | field=models.CharField(choices=[('Active', 'Active'), ('Paused', 'Paused'), ('Stopped', 'Stopped')], default='Active', max_length=20),
17 | ),
18 | migrations.AddField(
19 | model_name='offer',
20 | name='tracking_link',
21 | field=models.CharField(default='', max_length=1024),
22 | ),
23 | ]
24 |
--------------------------------------------------------------------------------
/offer/migrations/0013_offer_preview_link.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 2.2.6 on 2020-06-18 19:22
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('offer', '0012_auto_20200618_1922'),
10 | ]
11 |
12 | operations = [
13 | migrations.AddField(
14 | model_name='offer',
15 | name='preview_link',
16 | field=models.CharField(default='', max_length=1024),
17 | ),
18 | ]
19 |
--------------------------------------------------------------------------------
/offer/migrations/0014_advertiser.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 2.2.6 on 2020-06-18 19:25
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('offer', '0013_offer_preview_link'),
10 | ]
11 |
12 | operations = [
13 | migrations.CreateModel(
14 | name='Advertiser',
15 | fields=[
16 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
17 | ('company', models.CharField(max_length=64)),
18 | ('email', models.CharField(max_length=64)),
19 | ('contact_person', models.CharField(default='', max_length=64)),
20 | ('messenger', models.CharField(default='', max_length=64)),
21 | ('site', models.CharField(default='', max_length=64)),
22 | ('comment', models.TextField()),
23 | ],
24 | ),
25 | ]
26 |
--------------------------------------------------------------------------------
/offer/migrations/0015_offer_advertiser.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 2.2.6 on 2020-06-18 19:27
2 |
3 | from django.db import migrations, models
4 | import django.db.models.deletion
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ('offer', '0014_advertiser'),
11 | ]
12 |
13 | operations = [
14 | migrations.AddField(
15 | model_name='offer',
16 | name='advertiser',
17 | field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, to='offer.Advertiser'),
18 | ),
19 | ]
20 |
--------------------------------------------------------------------------------
/offer/migrations/0016_auto_20200629_2215.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.0.7 on 2020-06-29 22:15
2 |
3 | from django.db import migrations
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('offer', '0015_offer_advertiser'),
10 | ]
11 |
12 | operations = [
13 | migrations.AlterModelOptions(
14 | name='payout',
15 | options={'ordering': ('-payout',)},
16 | ),
17 | ]
18 |
--------------------------------------------------------------------------------
/offer/migrations/0017_offer_icon.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.0.7 on 2020-07-26 17:40
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('offer', '0016_auto_20200629_2215'),
10 | ]
11 |
12 | operations = [
13 | migrations.AddField(
14 | model_name='offer',
15 | name='icon',
16 | field=models.CharField(blank=True, default=None, max_length=255, null=True),
17 | ),
18 | ]
19 |
--------------------------------------------------------------------------------
/offer/migrations/0018_auto_20200726_1815.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.0.7 on 2020-07-26 18:15
2 |
3 | from django.db import migrations
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('offer', '0017_offer_icon'),
10 | ]
11 |
12 | operations = [
13 | migrations.AlterModelOptions(
14 | name='offer',
15 | options={'ordering': ('-id',)},
16 | ),
17 | ]
18 |
--------------------------------------------------------------------------------
/offer/migrations/0019_offer_description_html.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.0.7 on 2020-07-27 10:40
2 |
3 | from django.db import migrations
4 | import tinymce.models
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ('offer', '0018_auto_20200726_1815'),
11 | ]
12 |
13 | operations = [
14 | migrations.AddField(
15 | model_name='offer',
16 | name='description_html',
17 | field=tinymce.models.HTMLField(blank=True, default=''),
18 | ),
19 | ]
20 |
--------------------------------------------------------------------------------
/offer/migrations/0020_landing.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.0.7 on 2020-09-01 23:14
2 |
3 | from django.db import migrations, models
4 | import django.db.models.deletion
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ('offer', '0019_offer_description_html'),
11 | ]
12 |
13 | operations = [
14 | migrations.CreateModel(
15 | name='Landing',
16 | fields=[
17 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
18 | ('name', models.CharField(max_length=100)),
19 | ('url', models.CharField(default='', max_length=1024)),
20 | ('preview_url', models.CharField(default='', max_length=1024)),
21 | ('offer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='landings', to='offer.Offer')),
22 | ],
23 | ),
24 | ]
25 |
--------------------------------------------------------------------------------
/offer/migrations/0021_auto_20200903_2149.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.0.7 on 2020-09-03 21:49
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('offer', '0020_landing'),
10 | ]
11 |
12 | operations = [
13 | migrations.AlterField(
14 | model_name='offer',
15 | name='description',
16 | field=models.TextField(blank=True, default=''),
17 | ),
18 | ]
19 |
--------------------------------------------------------------------------------
/offer/migrations/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cpanova/cpa-network/1b5d1fbb3dfaa64441ee0be505b9eb75b44cb659/offer/migrations/__init__.py
--------------------------------------------------------------------------------
/offer/models.py:
--------------------------------------------------------------------------------
1 | from django.db import models
2 | from countries_plus.models import Country
3 | from tinymce import models as tinymce_models
4 |
5 |
6 | ACTIVE_STATUS = 'Active'
7 | PAUSED_STATUS = 'Paused'
8 | STOPPED_STATUS = 'Stopped'
9 | offer_statuses = (
10 | (ACTIVE_STATUS, 'Active'),
11 | (PAUSED_STATUS, 'Paused'),
12 | (STOPPED_STATUS, 'Stopped'),
13 | )
14 |
15 |
16 | class Offer(models.Model):
17 |
18 | class Meta:
19 | ordering = ('-id',)
20 |
21 | title = models.CharField(max_length=256, default='')
22 | description = models.TextField(blank=True, default='')
23 | description_html = tinymce_models.HTMLField(default='', blank=True)
24 | tracking_link = models.CharField(max_length=1024, default='')
25 | preview_link = models.CharField(max_length=1024, default='')
26 | countries = models.ManyToManyField(Country)
27 | categories = models.ManyToManyField('Category', blank=True)
28 | traffic_sources = models.ManyToManyField(
29 | 'TrafficSource', through='OfferTrafficSource', blank=True)
30 | status = models.CharField(max_length=20, choices=offer_statuses,
31 | default=ACTIVE_STATUS)
32 | icon = models.CharField(max_length=255,
33 | default=None, blank=True, null=True)
34 | advertiser = models.ForeignKey(
35 | 'Advertiser',
36 | on_delete=models.SET_NULL,
37 | null=True, blank=True, default=None)
38 |
39 | def __str__(self):
40 | return f"({self.id}) {self.title}"
41 |
42 |
43 | class Category(models.Model):
44 | name = models.CharField(max_length=256)
45 |
46 | def __str__(self):
47 | return self.name
48 |
49 |
50 | class TrafficSource(models.Model):
51 | name = models.CharField(max_length=256)
52 |
53 | def __str__(self):
54 | return self.name
55 |
56 |
57 | class OfferTrafficSource(models.Model):
58 | offer = models.ForeignKey(Offer, on_delete=models.CASCADE)
59 | traffic_source = models.ForeignKey(TrafficSource, on_delete=models.CASCADE)
60 | allowed = models.BooleanField(default=True)
61 |
62 |
63 | FIXED_PAYOUT = 'Fixed'
64 | PERCENT_PAYOUT = 'Percent'
65 | payout_types = (
66 | (FIXED_PAYOUT, 'Fixed'),
67 | (PERCENT_PAYOUT, 'Percent'),
68 | )
69 |
70 |
71 | class Goal(models.Model):
72 | name = models.CharField(max_length=64)
73 |
74 | def __str__(self):
75 | return self.name
76 |
77 |
78 | class Currency(models.Model):
79 | code = models.CharField(max_length=3)
80 | name = models.CharField(max_length=128)
81 |
82 | def __str__(self):
83 | return self.name
84 |
85 |
86 | class Payout(models.Model):
87 |
88 | class Meta:
89 | ordering = ('-payout',)
90 |
91 | revenue = models.DecimalField(max_digits=7, decimal_places=2)
92 | payout = models.DecimalField(max_digits=7, decimal_places=2)
93 | countries = models.ManyToManyField(Country)
94 | goal_value = models.CharField(max_length=20, default='1')
95 | type = models.CharField(
96 | max_length=20,
97 | choices=payout_types,
98 | default=FIXED_PAYOUT
99 | )
100 |
101 | currency = models.ForeignKey(
102 | Currency,
103 | on_delete=models.CASCADE
104 | )
105 |
106 | goal = models.ForeignKey(
107 | Goal,
108 | on_delete=models.SET_NULL,
109 | null=True,
110 | blank=True,
111 | )
112 |
113 | offer = models.ForeignKey(
114 | Offer,
115 | related_name='payouts',
116 | on_delete=models.CASCADE
117 | )
118 |
119 |
120 | class Advertiser(models.Model):
121 | company = models.CharField(max_length=64)
122 | email = models.CharField(max_length=64)
123 | contact_person = models.CharField(max_length=64, default='')
124 | messenger = models.CharField(max_length=64, default='')
125 | site = models.CharField(max_length=64, default='')
126 | comment = models.TextField()
127 |
128 | def __str__(self):
129 | return self.company
130 |
131 |
132 | class Landing(models.Model):
133 | name = models.CharField(max_length=100)
134 | url = models.CharField(max_length=1024, default='')
135 | preview_url = models.CharField(max_length=1024, default='')
136 |
137 | offer = models.ForeignKey(
138 | Offer,
139 | related_name='landings',
140 | on_delete=models.CASCADE
141 | )
142 |
143 | def __str__(self):
144 | return f'{self.id}: {self.name}'
145 |
--------------------------------------------------------------------------------
/offer/tasks/__init__.py:
--------------------------------------------------------------------------------
1 | from .cache_offers import cache_offers # noqa
2 |
--------------------------------------------------------------------------------
/offer/tasks/cache_offers.py:
--------------------------------------------------------------------------------
1 | import json
2 | import redis
3 | from project._celery import _celery
4 | from project.redis_conn import pool
5 | from ..models import Offer
6 |
7 |
8 | @_celery.task
9 | def cache_offers():
10 | redis_conn = redis.Redis(connection_pool=pool)
11 | for offer in Offer.objects.all():
12 | record = {
13 | 'tracking_link': offer.tracking_link
14 | }
15 | redis_conn.set(f'offers:{offer.id}', json.dumps(record))
16 |
--------------------------------------------------------------------------------
/permissions.txt:
--------------------------------------------------------------------------------
1 | every api defines it's own permissions
2 |
3 |
4 | User -* Groups (aka Roles)
5 |
6 |
7 | Permission(models.Model):
8 | name
9 | app
10 | groups**
11 |
12 |
13 | Apis:
14 | Affiliate
15 | Network
16 | AffiliateReport
17 | NetworkReport
18 |
19 |
20 | Роли
21 | аф манагер
22 | Affiliate
23 | read all
24 | write all
25 | Offer
26 | read only
27 | Advertiser
28 | no read
29 |
30 | админ
31 | *
--------------------------------------------------------------------------------
/postback/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cpanova/cpa-network/1b5d1fbb3dfaa64441ee0be505b9eb75b44cb659/postback/__init__.py
--------------------------------------------------------------------------------
/postback/admin.py:
--------------------------------------------------------------------------------
1 | from django.contrib import admin
2 | from .models import Postback, Log
3 |
4 |
5 | @admin.register(Postback)
6 | class PostbackAdmin(admin.ModelAdmin):
7 | list_display = (
8 | 'id',
9 | 'affiliate',
10 | 'offer',
11 | 'status',
12 | 'goal',
13 | 'url',
14 | )
15 |
16 |
17 | @admin.register(Log)
18 | class LogAdmin(admin.ModelAdmin):
19 | list_display = (
20 | 'created_at',
21 | 'affiliate',
22 | 'url',
23 | 'response_status',
24 | )
25 |
--------------------------------------------------------------------------------
/postback/apps.py:
--------------------------------------------------------------------------------
1 | from django.apps import AppConfig
2 |
3 |
4 | class PostbackConfig(AppConfig):
5 | name = 'postback'
6 |
--------------------------------------------------------------------------------
/postback/migrations/0001_initial.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.0.7 on 2020-08-26 22:08
2 |
3 | from django.conf import settings
4 | from django.db import migrations, models
5 | import django.db.models.deletion
6 |
7 |
8 | class Migration(migrations.Migration):
9 |
10 | initial = True
11 |
12 | dependencies = [
13 | migrations.swappable_dependency(settings.AUTH_USER_MODEL),
14 | ('offer', '0019_offer_description_html'),
15 | ]
16 |
17 | operations = [
18 | migrations.CreateModel(
19 | name='Postback',
20 | fields=[
21 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
22 | ('url', models.CharField(max_length=500)),
23 | ('status', models.CharField(choices=[('created', 'Created'), ('not_found', 'Not Found'), ('approved', 'Approved'), ('hold', 'Hold'), ('rejected', 'Rejected')], default='created', max_length=20)),
24 | ('goal', models.CharField(default='', max_length=20)),
25 | ('affiliate', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='postbacks', to=settings.AUTH_USER_MODEL)),
26 | ('offer', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='postbacks', to='offer.Offer')),
27 | ],
28 | ),
29 | ]
30 |
--------------------------------------------------------------------------------
/postback/migrations/0002_log.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.0.7 on 2020-08-26 22:13
2 |
3 | from django.conf import settings
4 | from django.db import migrations, models
5 | import django.db.models.deletion
6 |
7 |
8 | class Migration(migrations.Migration):
9 |
10 | dependencies = [
11 | migrations.swappable_dependency(settings.AUTH_USER_MODEL),
12 | ('postback', '0001_initial'),
13 | ]
14 |
15 | operations = [
16 | migrations.CreateModel(
17 | name='Log',
18 | fields=[
19 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
20 | ('created_at', models.DateTimeField(auto_now_add=True)),
21 | ('url', models.CharField(max_length=500)),
22 | ('response_status', models.CharField(default='', max_length=10)),
23 | ('response_text', models.TextField()),
24 | ('affiliate', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='postback_logs', to=settings.AUTH_USER_MODEL)),
25 | ],
26 | ),
27 | ]
28 |
--------------------------------------------------------------------------------
/postback/migrations/0003_auto_20200827_1244.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.0.7 on 2020-08-27 12:44
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('postback', '0002_log'),
10 | ]
11 |
12 | operations = [
13 | migrations.AlterField(
14 | model_name='postback',
15 | name='goal',
16 | field=models.CharField(blank=True, default='', max_length=20),
17 | ),
18 | ]
19 |
--------------------------------------------------------------------------------
/postback/migrations/0004_auto_20201018_0101.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.0.7 on 2020-10-18 01:01
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('postback', '0003_auto_20200827_1244'),
10 | ]
11 |
12 | operations = [
13 | migrations.AlterField(
14 | model_name='postback',
15 | name='status',
16 | field=models.CharField(choices=[('created', 'Created'), ('not_found', 'Not Found'), ('approved', 'Approved'), ('hold', 'Hold'), ('rejected', 'Rejected'), ('pending', 'Pending')], default='created', max_length=20),
17 | ),
18 | ]
19 |
--------------------------------------------------------------------------------
/postback/migrations/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cpanova/cpa-network/1b5d1fbb3dfaa64441ee0be505b9eb75b44cb659/postback/migrations/__init__.py
--------------------------------------------------------------------------------
/postback/models.py:
--------------------------------------------------------------------------------
1 | from django.db import models
2 | from django.contrib.auth import get_user_model
3 | from offer.models import Offer
4 | from tracker.models import conversion_statuses
5 |
6 |
7 | CREATED_STATUS = 'created'
8 | postback_statuses = (
9 | ('created', 'Created',),
10 | ('not_found', 'Not Found',),
11 | ) + conversion_statuses
12 |
13 |
14 | class Postback(models.Model):
15 | url = models.CharField(max_length=500)
16 | status = models.CharField(
17 | max_length=20,
18 | choices=postback_statuses,
19 | default=CREATED_STATUS)
20 | goal = models.CharField(max_length=20, blank=True, default="")
21 |
22 | offer = models.ForeignKey(
23 | Offer,
24 | related_name='postbacks',
25 | on_delete=models.CASCADE,
26 | null=True,
27 | blank=True
28 | )
29 |
30 | affiliate = models.ForeignKey(
31 | get_user_model(),
32 | related_name='postbacks',
33 | on_delete=models.CASCADE,
34 | )
35 |
36 |
37 | class Log(models.Model):
38 | created_at = models.DateTimeField(auto_now_add=True)
39 | url = models.CharField(max_length=500)
40 | response_status = models.CharField(max_length=10, default='')
41 | response_text = models.TextField()
42 |
43 | affiliate = models.ForeignKey(
44 | get_user_model(),
45 | related_name='postback_logs',
46 | on_delete=models.CASCADE,
47 | )
48 |
--------------------------------------------------------------------------------
/postback/tasks/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cpanova/cpa-network/1b5d1fbb3dfaa64441ee0be505b9eb75b44cb659/postback/tasks/__init__.py
--------------------------------------------------------------------------------
/postback/tasks/send_postback.py:
--------------------------------------------------------------------------------
1 | import requests
2 | from project._celery import _celery
3 | from ..models import Postback, Log
4 |
5 |
6 | def find_postbacks(affiliate_id, offer_id=None):
7 | return (
8 | Postback.objects
9 | .filter(affiliate_id=affiliate_id, offer_id=offer_id)
10 | )
11 |
12 |
13 | @_celery.task
14 | def send_postback(conversion):
15 | assert bool(conversion['offer_id'])
16 | assert bool(conversion['affiliate_id'])
17 |
18 | offer_id = conversion['offer_id']
19 | affiliate_id = conversion['affiliate_id']
20 |
21 | postbacks = find_postbacks(offer_id=offer_id, affiliate_id=affiliate_id)
22 | if not postbacks:
23 | postbacks = find_postbacks(affiliate_id=affiliate_id)
24 |
25 | for postback in postbacks:
26 | if postback.goal:
27 | if conversion['goal_value'] != postback.goal:
28 | continue
29 |
30 | url = replace_macro(postback.url, conversion)
31 |
32 | try:
33 | resp = requests.get(url)
34 |
35 | persist_log(affiliate_id, url, str(resp.status_code), resp.text)
36 | except requests.exceptions.Timeout:
37 | persist_log(affiliate_id, url, '', 'Timeout')
38 | except Exception as e:
39 | persist_log(affiliate_id, url, '', str(e))
40 |
41 |
42 | def persist_log(affiliate_id: int, url: str, status: str, text: str):
43 | log = Log()
44 | log.affiliate_id = affiliate_id
45 | log.url = url
46 | log.response_status = status
47 | log.response_text = text
48 | log.save()
49 |
50 |
51 | def replace_macro(url: str, data: dict) -> str:
52 | url = url.replace('{sub1}', data.get('sub1', ''))
53 | url = url.replace('{sub2}', data.get('sub2', ''))
54 | url = url.replace('{sub3}', data.get('sub3', ''))
55 | url = url.replace('{sub4}', data.get('sub4', ''))
56 | url = url.replace('{sub5}', data.get('sub5', ''))
57 | url = url.replace('{offer}', str(data.get('offer_id', '')))
58 | url = url.replace('{sum}', str(data.get('payout', '')))
59 | url = url.replace('{currency}', data.get('currency', '') or '')
60 | url = url.replace('{goal}', data.get('goal_value', ''))
61 |
62 | return url
63 |
--------------------------------------------------------------------------------
/postback/tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cpanova/cpa-network/1b5d1fbb3dfaa64441ee0be505b9eb75b44cb659/postback/tests/__init__.py
--------------------------------------------------------------------------------
/postback/tests/test_send_postback.py:
--------------------------------------------------------------------------------
1 | from unittest.mock import patch
2 | from django.test import TestCase
3 | from ..tasks.send_postback import send_postback
4 | from ..models import Postback
5 |
6 |
7 | @patch('postback.tasks.send_postback.persist_log', autospec=True)
8 | @patch('postback.tasks.send_postback.requests', autospec=True)
9 | @patch('postback.tasks.send_postback.find_postbacks', autospec=True)
10 | class TestSendPostback(TestCase):
11 |
12 | def setUp(self):
13 | super(TestSendPostback, self).setUp()
14 |
15 | def test_doesnt_send_postback(
16 | self, mocked_find_postbacks, mocked_requests, mocked_persist_log):
17 | mocked_find_postbacks.return_value = []
18 | conversion = {
19 | 'offer_id': 1,
20 | 'affiliate_id': 2,
21 | }
22 | send_postback(conversion)
23 | assert not mocked_requests.get.called, 'requests should not be called'
24 | assert not mocked_persist_log.called, 'log should not be called'
25 |
26 | def test_sends_postback(
27 | self, mocked_find_postbacks, mocked_requests, mocked_persist_log):
28 | url = "http://example.com/path?sub1={sub1}&sum={sum}"
29 | postback = Postback(url=url)
30 | mocked_find_postbacks.return_value = [postback]
31 | sub1 = '7a5b75c757d7c76d5c7'
32 | payout = 0.15
33 | conversion = {
34 | 'offer_id': 1,
35 | 'affiliate_id': 2,
36 | 'payout': payout,
37 | 'currency': 'USD',
38 | 'goal_value': '2',
39 | 'sub1': sub1,
40 | }
41 | send_postback(conversion)
42 | expected_url = f'http://example.com/path?sub1={sub1}&sum={payout}'
43 | (
44 | mocked_requests.get.assert_called_with(expected_url),
45 | 'requests should be called'
46 | )
47 | assert mocked_persist_log.called, 'persist log should be called'
48 |
49 | def test_goal_should_not_send_if_goal_doesnt_match(
50 | self, mocked_find_postbacks, mocked_requests, mocked_persist_log):
51 | postback = Postback(goal='2')
52 | mocked_find_postbacks.return_value = [postback]
53 | conversion = {
54 | 'offer_id': 1,
55 | 'affiliate_id': 2,
56 | 'goal_value': '1',
57 | }
58 | send_postback(conversion)
59 | assert not mocked_requests.get.called, 'requests should not be called'
60 |
61 | def test_goal_should_send_if_goal_match(
62 | self, mocked_find_postbacks, mocked_requests, mocked_persist_log):
63 | postback = Postback(goal='2')
64 | mocked_find_postbacks.return_value = [postback]
65 | conversion = {
66 | 'offer_id': 1,
67 | 'affiliate_id': 2,
68 | 'goal_value': '2',
69 | }
70 | send_postback(conversion)
71 | assert mocked_requests.get.called, 'requests should be called'
72 |
--------------------------------------------------------------------------------
/project/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cpanova/cpa-network/1b5d1fbb3dfaa64441ee0be505b9eb75b44cb659/project/__init__.py
--------------------------------------------------------------------------------
/project/_celery.py:
--------------------------------------------------------------------------------
1 | import os
2 | from celery import Celery
3 |
4 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'project.settings')
5 |
6 | from django.conf import settings # noqa
7 |
8 |
9 | _celery = Celery(
10 | 'cpa-platform',
11 | broker=settings.REDIS_URL
12 | )
13 |
14 | _celery.config_from_object('django.conf:settings', namespace='CELERY')
15 |
16 | _celery.autodiscover_tasks()
17 |
18 | # task_routes = {
19 | # 'campaigns.tasks.stats.push_sent': {'queue': 'stats:pushes'},
20 | # 'tracker.tasks.stats.*': {'queue': 'stats'},
21 | # }
22 |
23 | _celery.conf.update(
24 | task_serializer='json',
25 | accept_content=['json'], # Ignore other content
26 | timezone='Europe/Moscow',
27 | # task_routes=task_routes,
28 | )
29 |
30 | _celery.conf.beat_schedule = {
31 | 'cache-offers': {
32 | 'task': 'offer.tasks.cache_offers.cache_offers',
33 | 'schedule': 60
34 | },
35 | }
36 |
--------------------------------------------------------------------------------
/project/redis_conn.py:
--------------------------------------------------------------------------------
1 | import redis
2 | from django.conf import settings
3 |
4 | # redis_conn = Redis.from_url(settings.REDIS_URL)
5 | pool = redis.ConnectionPool.from_url(settings.REDIS_URL, max_connections=8)
6 |
--------------------------------------------------------------------------------
/project/settings/__init__.py:
--------------------------------------------------------------------------------
1 | try:
2 | from .local import * # noqa
3 | except ImportError:
4 | from .prod import * # noqa
5 |
--------------------------------------------------------------------------------
/project/settings/base.py:
--------------------------------------------------------------------------------
1 | import os
2 | import datetime
3 |
4 |
5 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...)
6 | BASE_DIR = os.path.dirname(
7 | os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
8 | )
9 |
10 |
11 | # SECURITY WARNING: don't run with debug turned on in production!
12 | DEBUG = False
13 |
14 | ALLOWED_HOSTS = ['*']
15 |
16 |
17 | # Application definition
18 |
19 | INSTALLED_APPS = [
20 | 'django.contrib.admin',
21 | 'django.contrib.auth',
22 | 'django.contrib.contenttypes',
23 | 'django.contrib.sessions',
24 | 'django.contrib.messages',
25 | 'django.contrib.staticfiles',
26 |
27 | 'rest_framework',
28 | 'rest_framework_swagger',
29 | 'corsheaders',
30 | 'countries_plus',
31 | 'tinymce',
32 | 'drf_spectacular',
33 |
34 | 'offer',
35 | 'affiliate',
36 | 'network',
37 | 'tracker',
38 | 'user_profile',
39 | 'api',
40 | 'postback',
41 | ]
42 |
43 |
44 | MIDDLEWARE = [
45 | 'django.middleware.security.SecurityMiddleware',
46 | 'whitenoise.middleware.WhiteNoiseMiddleware',
47 | 'django.contrib.sessions.middleware.SessionMiddleware',
48 | 'corsheaders.middleware.CorsMiddleware',
49 | 'django.middleware.common.CommonMiddleware',
50 | 'django.middleware.csrf.CsrfViewMiddleware',
51 | 'django.contrib.auth.middleware.AuthenticationMiddleware',
52 | 'django.contrib.messages.middleware.MessageMiddleware',
53 | 'django.middleware.clickjacking.XFrameOptionsMiddleware',
54 | ]
55 |
56 | ROOT_URLCONF = 'project.urls'
57 |
58 | TEMPLATES = [
59 | {
60 | 'BACKEND': 'django.template.backends.django.DjangoTemplates',
61 | 'DIRS': [],
62 | 'APP_DIRS': True,
63 | 'OPTIONS': {
64 | 'context_processors': [
65 | 'django.template.context_processors.debug',
66 | 'django.template.context_processors.request',
67 | 'django.contrib.auth.context_processors.auth',
68 | 'django.contrib.messages.context_processors.messages',
69 | ],
70 | },
71 | },
72 | ]
73 |
74 | WSGI_APPLICATION = 'project.wsgi.application'
75 |
76 |
77 | # Database
78 | # https://docs.djangoproject.com/en/2.2/ref/settings/#databases
79 |
80 | # DATABASES = {
81 | # 'default': {
82 | # 'ENGINE': 'django.db.backends.sqlite3',
83 | # 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
84 | # }
85 | # }
86 |
87 |
88 | # Password validation
89 | # https://docs.djangoproject.com/en/2.2/ref/settings/#auth-password-validators
90 |
91 | AUTH_PASSWORD_VALIDATORS = [
92 | {
93 | 'NAME': (
94 | 'django.contrib.auth.password_validation'
95 | '.UserAttributeSimilarityValidator'
96 | ),
97 | },
98 | {
99 | 'NAME': (
100 | 'django.contrib.auth.password_validation.MinimumLengthValidator'
101 | ),
102 | },
103 | {
104 | 'NAME': (
105 | 'django.contrib.auth.password_validation'
106 | '.CommonPasswordValidator'
107 | ),
108 | },
109 | {
110 | 'NAME': (
111 | 'django.contrib.auth.password_validation.NumericPasswordValidator'
112 | ),
113 | },
114 | ]
115 |
116 |
117 | # Internationalization
118 | # https://docs.djangoproject.com/en/2.2/topics/i18n/
119 |
120 | LANGUAGE_CODE = 'en-us'
121 |
122 | TIME_ZONE = 'UTC'
123 |
124 | USE_I18N = True
125 |
126 | USE_L10N = True
127 |
128 | USE_TZ = True
129 |
130 |
131 | # Static files (CSS, JavaScript, Images)
132 | # https://docs.djangoproject.com/en/2.2/howto/static-files/
133 |
134 | STATIC_URL = '/static/'
135 |
136 | STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles')
137 |
138 | STATICFILES_STORAGE = (
139 | 'whitenoise.storage.CompressedManifestStaticFilesStorage'
140 | )
141 |
142 | REST_FRAMEWORK = {
143 | 'DEFAULT_AUTHENTICATION_CLASSES': (
144 | 'rest_framework_jwt.authentication.JSONWebTokenAuthentication',
145 | 'rest_framework.authentication.BasicAuthentication',
146 | 'rest_framework.authentication.SessionAuthentication',
147 | ),
148 | 'DEFAULT_PARSER_CLASSES': (
149 | 'rest_framework.parsers.JSONParser',
150 | # 'rest_framework.parsers.MultiPartParser',
151 | # 'rest_framework.parsers.FormParser',
152 | ),
153 | 'DEFAULT_FILTER_BACKENDS': (
154 | 'django_filters.rest_framework.DjangoFilterBackend',
155 | ),
156 | 'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema',
157 | }
158 |
159 |
160 | JWT_AUTH = {
161 | 'JWT_EXPIRATION_DELTA': datetime.timedelta(seconds=60 * 60 * 24)
162 | }
163 |
164 |
165 | AUTHENTICATION_BACKENDS = [
166 | 'django.contrib.auth.backends.ModelBackend',
167 | ]
168 |
169 |
170 | CORS_ORIGIN_ALLOW_ALL = True
171 |
172 | CORS_ALLOW_METHODS = (
173 | 'DELETE',
174 | 'GET',
175 | 'OPTIONS',
176 | 'PATCH',
177 | 'POST',
178 | 'PUT',
179 | )
180 |
181 | REDIS_URL = os.environ['REDIS_URL']
182 |
183 | TRACKER_URL = os.environ['TRACKER_URL']
184 |
185 | IPSTACK_TOKEN = os.environ['IPSTACK_TOKEN']
186 |
--------------------------------------------------------------------------------
/project/settings/local.dist.py:
--------------------------------------------------------------------------------
1 | import dj_database_url
2 | from .base import * # noqa
3 |
4 |
5 | # SECURITY WARNING: keep the secret key used in production secret!
6 | SECRET_KEY = '4wsaeuncy$^*l_fycbwqi_2g)5a$b6a%x0@l@6k=xxxxxxxxxx'
7 |
8 | DEBUG = True
9 |
10 | DATABASE_URL = "postgresql://postgres:postgres@postgres/postgres"
11 |
12 | DATABASES = {
13 | 'default': dj_database_url.config(
14 | default=DATABASE_URL,
15 | engine='django.db.backends.postgresql_psycopg2')
16 | }
17 |
--------------------------------------------------------------------------------
/project/settings/prod.py:
--------------------------------------------------------------------------------
1 | import os
2 | import dj_database_url
3 | from .base import * # noqa
4 |
5 |
6 | SECRET_KEY = os.environ['DJ_SECRET_KEY']
7 |
8 |
9 | DATABASE_URL = os.environ['DATABASE_URL']
10 |
11 | DATABASES = {
12 | 'default': dj_database_url.config(
13 | default=DATABASE_URL,
14 | engine='django.db.backends.postgresql_psycopg2')
15 | }
16 |
--------------------------------------------------------------------------------
/project/urls.py:
--------------------------------------------------------------------------------
1 | """project URL Configuration
2 |
3 | The `urlpatterns` list routes URLs to views. For more information please see:
4 | https://docs.djangoproject.com/en/2.2/topics/http/urls/
5 | Examples:
6 | Function views
7 | 1. Add an import: from my_app import views
8 | 2. Add a URL to urlpatterns: path('', views.home, name='home')
9 | Class-based views
10 | 1. Add an import: from other_app.views import Home
11 | 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
12 | Including another URLconf
13 | 1. Import the include() function: from django.urls import include, path
14 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
15 | """
16 | from rest_framework_jwt.views import obtain_jwt_token
17 | from django.contrib import admin
18 | from django.urls import path, include
19 | from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView
20 |
21 |
22 | urlpatterns = [
23 | path('admin/', admin.site.urls),
24 |
25 | path('api/schema/', SpectacularAPIView.as_view(), name='schema'),
26 | path('api/', SpectacularSwaggerView.as_view(url_name='schema'), name='swagger-ui'),
27 |
28 | path('auth/', obtain_jwt_token),
29 |
30 | path('affiliate/', include('affiliate.urls')),
31 | path('network/', include('network.urls')),
32 | path('', include('tracker.urls')),
33 | path('api/', include('dictionaries.urls')),
34 | path('tinymce/', include('tinymce.urls')),
35 | path('api/', include('api.urls')),
36 | ]
37 |
--------------------------------------------------------------------------------
/project/wsgi.py:
--------------------------------------------------------------------------------
1 | """
2 | WSGI config for project project.
3 |
4 | It exposes the WSGI callable as a module-level variable named ``application``.
5 |
6 | For more information on this file, see
7 | https://docs.djangoproject.com/en/2.2/howto/deployment/wsgi/
8 | """
9 |
10 | import os
11 |
12 | from django.core.wsgi import get_wsgi_application
13 |
14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'project.settings')
15 |
16 | application = get_wsgi_application()
17 |
--------------------------------------------------------------------------------
/pytest.ini:
--------------------------------------------------------------------------------
1 | [pytest]
2 | DJANGO_SETTINGS_MODULE = project.settings
3 | python_files = tests.py test_*.py *_tests.py
4 |
--------------------------------------------------------------------------------
/requirements.dev.txt:
--------------------------------------------------------------------------------
1 | django==3.1.14
2 | flake8
3 | psycopg2-binary
4 | dj-database-url
5 | django-filter
6 | djangorestframework
7 | djangorestframework-jwt
8 | django-cors-headers
9 | drf-spectacular
10 | PyYAML
11 | whitenoise
12 | django-rest-swagger
13 | django-countries-plus
14 | requests
15 | iso8601
16 | celery
17 | maxminddb-geolite2
18 | redis
19 | honcho
20 | gunicorn
21 | django-tinymce
22 | mypy
23 | django-stubs
24 | djangorestframework-stubs
25 | webargs
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | amqp==5.1.1
2 | asgiref==3.5.2
3 | async-timeout==4.0.2
4 | billiard==3.6.4.0
5 | celery==5.2.7
6 | certifi==2022.9.24
7 | charset-normalizer==2.1.1
8 | click==8.1.3
9 | click-didyoumean==0.3.0
10 | click-plugins==1.1.1
11 | click-repl==0.2.0
12 | coreapi==2.3.3
13 | coreschema==0.0.4
14 | dj-database-url==0.5.0
15 | Django==3.1.14
16 | django-cors-headers==3.11.0
17 | django-countries-plus==2.1.0
18 | django-filter==21.1
19 | django-rest-swagger==2.2.0
20 | django-stubs==1.13.0
21 | django-stubs-ext==0.7.0
22 | django-tinymce==3.5.0
23 | djangorestframework==3.14.0
24 | djangorestframework-jwt==1.11.0
25 | djangorestframework-stubs==1.8.0
26 | drf-spectacular==0.27.0
27 | flake8==6.0.0
28 | gunicorn==20.1.0
29 | honcho==1.1.0
30 | idna==3.4
31 | iso8601==1.1.0
32 | itypes==1.2.0
33 | Jinja2==3.1.2
34 | kombu==5.2.4
35 | MarkupSafe==2.1.1
36 | marshmallow==3.19.0
37 | maxminddb==2.2.0
38 | maxminddb-geolite2==2018.703
39 | mccabe==0.7.0
40 | mypy==0.991
41 | mypy-extensions==0.4.3
42 | openapi-codec==1.3.2
43 | packaging==21.3
44 | prompt-toolkit==3.0.33
45 | psycopg2-binary==2.9.5
46 | pycodestyle==2.10.0
47 | pyflakes==3.0.1
48 | PyJWT==1.7.1
49 | pyparsing==3.0.9
50 | pytz==2022.6
51 | PyYAML==6.0
52 | redis==4.3.5
53 | requests==2.28.1
54 | simplejson==3.18.0
55 | six==1.16.0
56 | sqlparse==0.4.3
57 | tomli==2.0.1
58 | types-pytz==2022.6.0.1
59 | types-PyYAML==6.0.12.2
60 | types-requests==2.28.11.5
61 | types-urllib3==1.26.25.4
62 | typing_extensions==4.4.0
63 | uritemplate==4.1.1
64 | urllib3==1.26.13
65 | vine==5.0.0
66 | wcwidth==0.2.5
67 | webargs==8.2.0
68 | whitenoise==6.2.0
69 |
--------------------------------------------------------------------------------
/tracker/__init__.py:
--------------------------------------------------------------------------------
1 | default_app_config = 'tracker.apps.TrackerConfig'
2 |
--------------------------------------------------------------------------------
/tracker/admin.py:
--------------------------------------------------------------------------------
1 | from django.contrib import admin
2 | from .models import Click, Conversion
3 |
4 |
5 | admin.site.register(Click)
6 |
7 |
8 | @admin.register(Conversion)
9 | class ConversionAdmin(admin.ModelAdmin):
10 | list_display = (
11 | 'id',
12 | 'created_at',
13 | 'affiliate',
14 | 'offer',
15 | 'revenue',
16 | 'payout',
17 | 'goal',
18 | 'status',
19 | 'ip',
20 | 'country',
21 | )
22 |
--------------------------------------------------------------------------------
/tracker/apps.py:
--------------------------------------------------------------------------------
1 | from django.apps import AppConfig
2 |
3 |
4 | class TrackerConfig(AppConfig):
5 | name = 'tracker'
6 |
7 | def ready(self):
8 | super(TrackerConfig, self).ready()
9 | import tracker.signals # noqa: F401
10 |
--------------------------------------------------------------------------------
/tracker/dao.py:
--------------------------------------------------------------------------------
1 | import json
2 | import redis
3 | from offer.models import Payout
4 | from project.redis_conn import pool
5 | from typing import Optional, Dict
6 |
7 |
8 | def any(predicate, collection):
9 | return bool(len(list(filter(predicate, collection))))
10 |
11 |
12 | def first(collection):
13 | try:
14 | return collection[0]
15 | except IndexError:
16 | return None
17 |
18 |
19 | def find_payout(offer_id: int, country: str, goal: str) -> Payout:
20 | return (
21 | Payout.objects
22 | .filter(
23 | offer_id=offer_id,
24 | goal_value=goal,
25 | countries__in=[country]
26 | )
27 | .first()
28 | )
29 | # payouts = Payout.objects.filter(offer_id=offer_id, goal_value=goal)
30 | # return (
31 | # first(
32 | # list(
33 | # filter(
34 | # lambda p:
35 | # any(
36 | # lambda c:
37 | # c.code == country, p.countries),
38 | # payouts))))
39 |
40 |
41 | class TrackerCache(object):
42 |
43 | @staticmethod
44 | def get_offer(id: int) -> Optional[Dict]:
45 | redis_conn = redis.Redis(connection_pool=pool)
46 | resp = redis_conn.get(f"offers:{id}")
47 | if resp:
48 | return json.loads(resp)
49 | return None
50 |
51 | # @staticmethod
52 | # def get_user(id: int) -> dict:
53 | # redis_conn = redis.Redis(connection_pool=pool)
54 | # resp = redis_conn.get(f"users:{id}")
55 | # if resp:
56 | # return json.loads(resp)
57 | # return None
58 |
--------------------------------------------------------------------------------
/tracker/migrations/0001_initial.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 2.2.6 on 2020-06-17 17:44
2 |
3 | from django.conf import settings
4 | from django.db import migrations, models
5 | import django.db.models.deletion
6 | import uuid
7 |
8 |
9 | class Migration(migrations.Migration):
10 |
11 | initial = True
12 |
13 | dependencies = [
14 | migrations.swappable_dependency(settings.AUTH_USER_MODEL),
15 | ('offer', '0004_auto_20191009_1712'),
16 | ]
17 |
18 | operations = [
19 | migrations.CreateModel(
20 | name='Conversion',
21 | fields=[
22 | ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
23 | ('click_id', models.UUIDField(editable=False)),
24 | ('created_at', models.DateTimeField(auto_now_add=True)),
25 | ('click_date', models.DateTimeField()),
26 | ('sub1', models.CharField(default='', max_length=500)),
27 | ('sub2', models.CharField(default='', max_length=500)),
28 | ('sub3', models.CharField(default='', max_length=500)),
29 | ('sub4', models.CharField(default='', max_length=500)),
30 | ('sub5', models.CharField(default='', max_length=500)),
31 | ('revenue', models.DecimalField(decimal_places=2, max_digits=7)),
32 | ('payout', models.DecimalField(decimal_places=2, max_digits=7)),
33 | ('ip', models.GenericIPAddressField()),
34 | ('country', models.CharField(default='', max_length=2)),
35 | ('ua', models.CharField(default='', max_length=200)),
36 | ('goal', models.CharField(default='', max_length=20)),
37 | ('status', models.CharField(choices=[('approved', 'Approved'), ('hold', 'Hold'), ('rejected', 'Rejected')], default='rejected', max_length=10)),
38 | ('sum', models.FloatField(default=0.0)),
39 | ('affiliate', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='conversions', to=settings.AUTH_USER_MODEL)),
40 | ('offer', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='conversions', to='offer.Offer')),
41 | ],
42 | ),
43 | migrations.CreateModel(
44 | name='Click',
45 | fields=[
46 | ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
47 | ('created_at', models.DateTimeField(auto_now_add=True)),
48 | ('sub1', models.CharField(default='', max_length=500)),
49 | ('sub2', models.CharField(default='', max_length=500)),
50 | ('sub3', models.CharField(default='', max_length=500)),
51 | ('sub4', models.CharField(default='', max_length=500)),
52 | ('sub5', models.CharField(default='', max_length=500)),
53 | ('ip', models.GenericIPAddressField()),
54 | ('country', models.CharField(default='', max_length=2)),
55 | ('ua', models.CharField(default='', max_length=200)),
56 | ('revenue', models.DecimalField(decimal_places=2, max_digits=7)),
57 | ('payout', models.DecimalField(decimal_places=2, max_digits=7)),
58 | ('affiliate', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='clicks', to=settings.AUTH_USER_MODEL)),
59 | ('offer', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='clicks', to='offer.Offer')),
60 | ],
61 | ),
62 | ]
63 |
--------------------------------------------------------------------------------
/tracker/migrations/0002_auto_20200618_1524.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 2.2.6 on 2020-06-18 15:24
2 |
3 | from django.conf import settings
4 | from django.db import migrations, models
5 | import django.db.models.deletion
6 |
7 |
8 | class Migration(migrations.Migration):
9 |
10 | dependencies = [
11 | migrations.swappable_dependency(settings.AUTH_USER_MODEL),
12 | ('tracker', '0001_initial'),
13 | ]
14 |
15 | operations = [
16 | migrations.AddField(
17 | model_name='click',
18 | name='affiliate_manager',
19 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL),
20 | ),
21 | migrations.AddField(
22 | model_name='conversion',
23 | name='affiliate_manager',
24 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL),
25 | ),
26 | ]
27 |
--------------------------------------------------------------------------------
/tracker/migrations/0003_auto_20200629_2057.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.0.7 on 2020-06-29 20:57
2 |
3 | from django.db import migrations
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('tracker', '0002_auto_20200618_1524'),
10 | ]
11 |
12 | operations = [
13 | migrations.RenameField(
14 | model_name='conversion',
15 | old_name='goal',
16 | new_name='goal_value',
17 | ),
18 | ]
19 |
--------------------------------------------------------------------------------
/tracker/migrations/0004_conversion_goal.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.0.7 on 2020-06-29 21:10
2 |
3 | from django.db import migrations, models
4 | import django.db.models.deletion
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ('offer', '0015_offer_advertiser'),
11 | ('tracker', '0003_auto_20200629_2057'),
12 | ]
13 |
14 | operations = [
15 | migrations.AddField(
16 | model_name='conversion',
17 | name='goal',
18 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='offer.Goal'),
19 | ),
20 | ]
21 |
--------------------------------------------------------------------------------
/tracker/migrations/0005_conversion_currency.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.0.7 on 2020-06-29 21:20
2 |
3 | from django.db import migrations, models
4 | import django.db.models.deletion
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ('offer', '0015_offer_advertiser'),
11 | ('tracker', '0004_conversion_goal'),
12 | ]
13 |
14 | operations = [
15 | migrations.AddField(
16 | model_name='conversion',
17 | name='currency',
18 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='offer.Currency'),
19 | ),
20 | ]
21 |
--------------------------------------------------------------------------------
/tracker/migrations/0006_conversion_comment.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.0.7 on 2020-06-29 22:15
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('tracker', '0005_conversion_currency'),
10 | ]
11 |
12 | operations = [
13 | migrations.AddField(
14 | model_name='conversion',
15 | name='comment',
16 | field=models.CharField(blank=True, default='', max_length=128),
17 | ),
18 | ]
19 |
--------------------------------------------------------------------------------
/tracker/migrations/0007_auto_20200803_1648.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.0.7 on 2020-08-03 16:48
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('tracker', '0006_conversion_comment'),
10 | ]
11 |
12 | operations = [
13 | migrations.AlterField(
14 | model_name='conversion',
15 | name='click_id',
16 | field=models.UUIDField(default=None, editable=False, null=True),
17 | ),
18 | ]
19 |
--------------------------------------------------------------------------------
/tracker/migrations/0008_auto_20200803_1822.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.0.7 on 2020-08-03 18:22
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('tracker', '0007_auto_20200803_1648'),
10 | ]
11 |
12 | operations = [
13 | migrations.AlterField(
14 | model_name='conversion',
15 | name='click_date',
16 | field=models.DateTimeField(default=None, null=True),
17 | ),
18 | ]
19 |
--------------------------------------------------------------------------------
/tracker/migrations/0009_auto_20200803_1824.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.0.7 on 2020-08-03 18:24
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('tracker', '0008_auto_20200803_1822'),
10 | ]
11 |
12 | operations = [
13 | migrations.AlterField(
14 | model_name='conversion',
15 | name='payout',
16 | field=models.DecimalField(decimal_places=2, default=0.0, max_digits=7),
17 | ),
18 | migrations.AlterField(
19 | model_name='conversion',
20 | name='revenue',
21 | field=models.DecimalField(decimal_places=2, default=0.0, max_digits=7),
22 | ),
23 | ]
24 |
--------------------------------------------------------------------------------
/tracker/migrations/0010_auto_20200803_1825.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.0.7 on 2020-08-03 18:25
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('tracker', '0009_auto_20200803_1824'),
10 | ]
11 |
12 | operations = [
13 | migrations.AlterField(
14 | model_name='conversion',
15 | name='ip',
16 | field=models.GenericIPAddressField(default=None, null=True),
17 | ),
18 | ]
19 |
--------------------------------------------------------------------------------
/tracker/migrations/0011_auto_20200826_2206.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.0.7 on 2020-08-26 22:06
2 |
3 | from django.db import migrations
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('tracker', '0010_auto_20200803_1825'),
10 | ]
11 |
12 | operations = [
13 | migrations.AlterModelOptions(
14 | name='conversion',
15 | options={'ordering': ('-created_at',)},
16 | ),
17 | ]
18 |
--------------------------------------------------------------------------------
/tracker/migrations/0012_auto_20200914_1037.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.0.7 on 2020-09-14 10:37
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('tracker', '0011_auto_20200826_2206'),
10 | ]
11 |
12 | operations = [
13 | migrations.AlterField(
14 | model_name='conversion',
15 | name='click_date',
16 | field=models.DateTimeField(blank=True, default=None, null=True),
17 | ),
18 | migrations.AlterField(
19 | model_name='conversion',
20 | name='country',
21 | field=models.CharField(blank=True, default='', max_length=2),
22 | ),
23 | migrations.AlterField(
24 | model_name='conversion',
25 | name='ip',
26 | field=models.GenericIPAddressField(blank=True, default=None, null=True),
27 | ),
28 | migrations.AlterField(
29 | model_name='conversion',
30 | name='sub1',
31 | field=models.CharField(blank=True, default='', max_length=500),
32 | ),
33 | migrations.AlterField(
34 | model_name='conversion',
35 | name='sub2',
36 | field=models.CharField(blank=True, default='', max_length=500),
37 | ),
38 | migrations.AlterField(
39 | model_name='conversion',
40 | name='sub3',
41 | field=models.CharField(blank=True, default='', max_length=500),
42 | ),
43 | migrations.AlterField(
44 | model_name='conversion',
45 | name='sub4',
46 | field=models.CharField(blank=True, default='', max_length=500),
47 | ),
48 | migrations.AlterField(
49 | model_name='conversion',
50 | name='sub5',
51 | field=models.CharField(blank=True, default='', max_length=500),
52 | ),
53 | migrations.AlterField(
54 | model_name='conversion',
55 | name='ua',
56 | field=models.CharField(blank=True, default='', max_length=200),
57 | ),
58 | ]
59 |
--------------------------------------------------------------------------------
/tracker/migrations/0013_auto_20201018_0101.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.0.7 on 2020-10-18 01:01
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('tracker', '0012_auto_20200914_1037'),
10 | ]
11 |
12 | operations = [
13 | migrations.AlterField(
14 | model_name='conversion',
15 | name='status',
16 | field=models.CharField(choices=[('approved', 'Approved'), ('hold', 'Hold'), ('rejected', 'Rejected'), ('pending', 'Pending')], default='rejected', max_length=10),
17 | ),
18 | ]
19 |
--------------------------------------------------------------------------------
/tracker/migrations/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cpanova/cpa-network/1b5d1fbb3dfaa64441ee0be505b9eb75b44cb659/tracker/migrations/__init__.py
--------------------------------------------------------------------------------
/tracker/models.py:
--------------------------------------------------------------------------------
1 | import uuid
2 | from django.db import models
3 | from offer.models import Offer, Goal, Currency
4 | from django.contrib.auth import get_user_model
5 |
6 |
7 | class Click(models.Model):
8 | id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
9 | created_at = models.DateTimeField(auto_now_add=True)
10 | sub1 = models.CharField(max_length=500, default="")
11 | sub2 = models.CharField(max_length=500, default="")
12 | sub3 = models.CharField(max_length=500, default="")
13 | sub4 = models.CharField(max_length=500, default="")
14 | sub5 = models.CharField(max_length=500, default="")
15 | ip = models.GenericIPAddressField()
16 | country = models.CharField(max_length=2, default="")
17 | ua = models.CharField(max_length=200, default="")
18 | revenue = models.DecimalField(max_digits=7, decimal_places=2)
19 | payout = models.DecimalField(max_digits=7, decimal_places=2)
20 |
21 | offer = models.ForeignKey(
22 | Offer,
23 | related_name='clicks',
24 | on_delete=models.SET_NULL,
25 | null=True
26 | )
27 |
28 | affiliate = models.ForeignKey(
29 | get_user_model(),
30 | related_name='clicks',
31 | on_delete=models.SET_NULL,
32 | null=True
33 | )
34 |
35 | affiliate_manager = models.ForeignKey(
36 | get_user_model(),
37 | # related_name='clicks',
38 | on_delete=models.SET_NULL,
39 | null=True,
40 | blank=True,
41 | )
42 |
43 |
44 | APPROVED_STATUS = 'approved'
45 | HOLD_STATUS = 'hold'
46 | REJECTED_STATUS = 'rejected'
47 | PENDING_STATUS = 'pending'
48 | conversion_statuses = (
49 | (APPROVED_STATUS, 'Approved',),
50 | (HOLD_STATUS, 'Hold',),
51 | (REJECTED_STATUS, 'Rejected',),
52 | (PENDING_STATUS, 'Pending',),
53 | )
54 |
55 |
56 | class Conversion(models.Model):
57 |
58 | class Meta:
59 | ordering = ('-created_at',)
60 |
61 | id = models.UUIDField(
62 | primary_key=True, default=uuid.uuid4, editable=False)
63 | click_id = models.UUIDField(editable=False, null=True, default=None)
64 | created_at = models.DateTimeField(auto_now_add=True)
65 | click_date = models.DateTimeField(null=True, default=None, blank=True)
66 | sub1 = models.CharField(max_length=500, default="", blank=True)
67 | sub2 = models.CharField(max_length=500, default="", blank=True)
68 | sub3 = models.CharField(max_length=500, default="", blank=True)
69 | sub4 = models.CharField(max_length=500, default="", blank=True)
70 | sub5 = models.CharField(max_length=500, default="", blank=True)
71 | revenue = models.DecimalField(max_digits=7, decimal_places=2, default=.0)
72 | payout = models.DecimalField(max_digits=7, decimal_places=2, default=.0)
73 | ip = models.GenericIPAddressField(null=True, default=None, blank=True)
74 | country = models.CharField(max_length=2, default="", blank=True)
75 | ua = models.CharField(max_length=200, default="", blank=True)
76 | goal_value = models.CharField(max_length=20, default="")
77 | status = models.CharField(
78 | max_length=10, choices=conversion_statuses, default=REJECTED_STATUS)
79 | sum = models.FloatField(default=0.0)
80 | comment = models.CharField(max_length=128, default='', blank=True)
81 |
82 | goal = models.ForeignKey(
83 | Goal,
84 | on_delete=models.SET_NULL,
85 | null=True,
86 | blank=True,
87 | )
88 |
89 | currency = models.ForeignKey(
90 | Currency,
91 | on_delete=models.SET_NULL,
92 | null=True,
93 | blank=True,
94 | )
95 |
96 | offer = models.ForeignKey(
97 | Offer,
98 | related_name='conversions',
99 | on_delete=models.SET_NULL,
100 | null=True
101 | )
102 |
103 | affiliate = models.ForeignKey(
104 | get_user_model(),
105 | related_name='conversions',
106 | on_delete=models.SET_NULL,
107 | null=True
108 | )
109 |
110 | affiliate_manager = models.ForeignKey(
111 | get_user_model(),
112 | # related_name='conversions',
113 | on_delete=models.SET_NULL,
114 | null=True,
115 | blank=True,
116 | )
117 |
--------------------------------------------------------------------------------
/tracker/signals.py:
--------------------------------------------------------------------------------
1 | from django.dispatch import receiver
2 | from django.db.models.signals import post_save
3 | from .models import Conversion
4 | from postback.tasks.send_postback import send_postback
5 |
6 |
7 | @receiver(post_save, sender=Conversion)
8 | def on_conversion_created(sender, instance, created, **kwargs):
9 | if created:
10 | cv = {
11 | 'affiliate_id': instance.affiliate_id,
12 | 'offer_id': instance.offer_id,
13 | 'sub1': instance.sub1,
14 | 'sub2': instance.sub2,
15 | 'sub3': instance.sub3,
16 | 'sub4': instance.sub4,
17 | 'sub5': instance.sub5,
18 | 'payout': instance.payout,
19 | 'goal_value': instance.goal_value,
20 | 'currency': instance.currency.code if instance.currency else '',
21 | }
22 | send_postback.delay(cv)
23 |
--------------------------------------------------------------------------------
/tracker/tasks/__init__.py:
--------------------------------------------------------------------------------
1 | # from .sync import sync # noqa
2 | # from .click import click # noqa
3 | # from .conversion import conversion # noqa
4 | # from .on_deposit import on_deposit # noqa
5 |
--------------------------------------------------------------------------------
/tracker/tasks/click.py:
--------------------------------------------------------------------------------
1 | from geolite2 import geolite2
2 | from django.contrib.auth import get_user_model
3 | from project._celery import _celery
4 | from tracker.models import Click
5 | from ext.ipapi import API, Err as IpstackErr
6 |
7 |
8 | def detect_country(ip: str) -> str:
9 | reader = geolite2.reader()
10 | ip_info = reader.get(ip) or {}
11 | country = ip_info.get("country", {}).get("iso_code", "")
12 | return country
13 |
14 |
15 | def detect_country_service(ip: str) -> str:
16 | api = API()
17 | try:
18 | resp = api.query(ip)
19 | except IpstackErr:
20 | return ''
21 | return resp.country_code
22 |
23 |
24 | @_celery.task
25 | def click(data):
26 | country = detect_country_service(data["ip"])
27 |
28 | try:
29 | user = get_user_model().objects.get(pk=data['pid'])
30 | except get_user_model().DoesNotExist:
31 | msg = f"affiliate {data['pid']} not found"
32 | print(msg)
33 | return msg
34 |
35 | click = Click()
36 | click.id = data['click_id']
37 | click.offer_id = data['offer_id']
38 | click.affiliate_id = data['pid']
39 | click.affiliate_manager = user.profile.manager
40 | click.sub1 = data['sub1']
41 | click.sub2 = data['sub2']
42 | click.sub3 = data['sub3']
43 | click.sub4 = data['sub4']
44 | click.sub5 = data['sub5']
45 | click.revenue = 0
46 | click.payout = 0
47 | click.ip = data['ip']
48 | click.country = country
49 | click.ua = data['ua']
50 | click.save()
51 |
52 | return f"Click created: {click.id}"
53 |
--------------------------------------------------------------------------------
/tracker/tasks/conversion.py:
--------------------------------------------------------------------------------
1 | from project._celery import _celery
2 | from tracker.models import (
3 | Click,
4 | Conversion,
5 | HOLD_STATUS,
6 | REJECTED_STATUS,
7 | )
8 | from offer.models import Payout
9 |
10 |
11 | @_celery.task
12 | def conversion(data):
13 | try:
14 | click = Click.objects.get(pk=data['click_id'])
15 | except Click.DoesNotExist:
16 | return f"Click not found. click_id: {data['click_id']}"
17 |
18 | existing_conversion = (
19 | Conversion.objects
20 | .filter(click_id=click.id, goal_value=data['goal'])
21 | .first()
22 | )
23 | if existing_conversion and data.get('status'):
24 | if existing_conversion.status == HOLD_STATUS:
25 | existing_conversion.status = data.get('status')
26 | existing_conversion.save()
27 | return f"Processed conversion for click_id: {data['click_id']}"
28 |
29 | duplicate = bool(existing_conversion)
30 |
31 | conversion = Conversion()
32 |
33 | payout = (
34 | Payout.objects
35 | .filter(
36 | offer_id=click.offer_id,
37 | goal_value=data['goal'],
38 | countries__in=[click.country]
39 | )
40 | .first()
41 | )
42 |
43 | if payout:
44 | conversion.revenue = payout.revenue
45 | conversion.payout = payout.payout
46 | conversion.goal = payout.goal
47 | conversion.currency = payout.currency
48 |
49 | if data.get('status'):
50 | conversion.status = data.get('status')
51 | else:
52 | conversion.status = HOLD_STATUS
53 |
54 | if duplicate:
55 | conversion.status = REJECTED_STATUS
56 | conversion.comment = 'Duplicate Click ID'
57 | else:
58 | conversion.revenue = 0
59 | conversion.payout = 0
60 | conversion.status = REJECTED_STATUS
61 | conversion.comment = 'Payout not found'
62 |
63 | conversion.click_id = click.id
64 | conversion.click_date = click.created_at
65 |
66 | conversion.offer_id = click.offer.id
67 | conversion.affiliate_id = click.affiliate.id
68 | conversion.affiliate_manager = click.affiliate.profile.manager
69 | conversion.sub1 = click.sub1
70 | conversion.sub2 = click.sub2
71 | conversion.sub3 = click.sub3
72 | conversion.sub4 = click.sub4
73 | conversion.sub5 = click.sub5
74 | conversion.ip = click.ip
75 | conversion.ua = click.ua
76 | conversion.country = click.country
77 |
78 | conversion.goal_value = data['goal']
79 | conversion.sum = data['sum']
80 |
81 | conversion.save()
82 |
83 | return f"Processed conversion for click_id: {data['click_id']}"
84 |
--------------------------------------------------------------------------------
/tracker/tasks/sync.py:
--------------------------------------------------------------------------------
1 | from project._celery import _celery
2 |
3 |
4 | @_celery.task
5 | def sync():
6 | return 'sync done'
7 |
--------------------------------------------------------------------------------
/tracker/tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cpanova/cpa-network/1b5d1fbb3dfaa64441ee0be505b9eb75b44cb659/tracker/tests/__init__.py
--------------------------------------------------------------------------------
/tracker/tests/test_click_task.py:
--------------------------------------------------------------------------------
1 | import uuid
2 | from django.test import TestCase
3 | from ..tasks.click import click
4 | from ..models import Click
5 | from django.contrib.auth import get_user_model
6 | from offer.models import Offer
7 |
8 |
9 | class TestClickTask(TestCase):
10 |
11 | def setUp(self):
12 | super(TestClickTask, self).setUp()
13 | self.user = (
14 | get_user_model().objects
15 | .create_user(username='aff', password='1234')
16 | )
17 | self.manager = (
18 | get_user_model().objects
19 | .create_user(username='manager', password='1234')
20 | )
21 | self.user.profile.manager = self.manager
22 | self.user.profile.save()
23 | self.offer = Offer.objects.create(title='blabla', description='blabla')
24 |
25 | def test_click_create(self):
26 | click_id = uuid.uuid4()
27 | data = {
28 | 'click_id': click_id,
29 | 'offer_id': self.offer.pk,
30 | 'pid': self.user.pk,
31 | 'ip': '4.4.4.4',
32 | 'ua': 'Mozilla /5.0',
33 | 'sub1': '',
34 | 'sub2': '',
35 | 'sub3': '',
36 | 'sub4': '',
37 | 'sub5': ''
38 | }
39 | click(data)
40 | cl = Click.objects.get(pk=click_id)
41 | self.assertTrue(Click.objects.count())
42 | self.assertEqual(cl.country, 'US')
43 | self.assertEqual(cl.affiliate_manager_id, self.manager.pk)
44 |
--------------------------------------------------------------------------------
/tracker/tests/test_click_view.py:
--------------------------------------------------------------------------------
1 | from django.test import TestCase
2 | from unittest import mock
3 | from django.urls import reverse
4 |
5 |
6 | class TestClickHanlder(TestCase):
7 |
8 | @mock.patch('tracker.dao.TrackerCache')
9 | def test_it_returns_400(self, mock_tracker_cache):
10 | url = reverse('tracker-click')
11 | response = self.client.get(url)
12 | self.assertEqual(400, response.status_code)
13 |
14 | @mock.patch('tracker.dao.TrackerCache.get_offer', return_value=None)
15 | def test_it_returns_404(self, mock_tracker_cache):
16 | url = reverse('tracker-click')
17 | response = self.client.get(url, {'pid': 1, 'offer_id': 1})
18 | self.assertEqual(404, response.status_code)
19 |
20 | @mock.patch('tracker.views.click_task')
21 | @mock.patch(
22 | 'tracker.dao.TrackerCache.get_offer',
23 | return_value={'tracking_link': 'http://example.com'}
24 | )
25 | def test_it_returns_302(self, mock_tracker_cache, mock_click_task):
26 | url = reverse('tracker-click')
27 | response = self.client.get(
28 | url,
29 | {'pid': 1, 'offer_id': 1},
30 | HTTP_USER_AGENT='Mozilla/5.0'
31 | )
32 | self.assertEqual(302, response.status_code)
33 | self.assertTrue(mock_click_task.delay.called)
34 |
--------------------------------------------------------------------------------
/tracker/tests/test_conversion_task.py:
--------------------------------------------------------------------------------
1 | from django.test import TestCase
2 | from countries_plus.models import Country
3 | from ..tasks.conversion import conversion
4 | from ..models import Click, Conversion
5 | from django.contrib.auth import get_user_model
6 | from offer.models import Offer, Payout, Currency
7 |
8 |
9 | class TestConversionTask(TestCase):
10 |
11 | def setUp(self):
12 | super(TestConversionTask, self).setUp()
13 | self.user = (
14 | get_user_model().objects
15 | .create_user(username='aff', password='1234')
16 | )
17 | self.offer = Offer.objects.create(
18 | id=1, title='blabla', description='blabla'
19 | )
20 |
21 | def test_conversion_create(self):
22 | cl = Click.objects.create(
23 | ip='4.4.4.4',
24 | revenue=0,
25 | payout=0,
26 | offer=self.offer,
27 | affiliate=self.user
28 | )
29 | data = {
30 | 'click_id': cl.id,
31 | 'goal': 1,
32 | 'sum': 0
33 | }
34 | conversion(data)
35 | self.assertTrue(
36 | Conversion.objects
37 | .filter(click_id=cl.id)
38 | .count()
39 | )
40 |
41 | def test_with_status(self):
42 | cl = Click.objects.create(
43 | ip='4.4.4.4',
44 | country='US',
45 | revenue=0,
46 | payout=0,
47 | offer=self.offer,
48 | affiliate=self.user
49 | )
50 | p = Payout.objects.create(
51 | revenue=0,
52 | payout=0,
53 | goal_value='1',
54 | offer=cl.offer,
55 | currency=Currency.objects.create(code='USD', name='USD'),
56 | )
57 | p.countries.add(
58 | Country.objects.create(
59 | iso='US', iso3='USA', iso_numeric=1, name='USA'
60 | )
61 | )
62 | data = {
63 | 'click_id': cl.id,
64 | 'goal': 1,
65 | 'sum': 0,
66 | 'status': 'approved'
67 | }
68 | conversion(data)
69 | cv = (
70 | Conversion.objects
71 | .filter(click_id=cl.id)
72 | .first())
73 | self.assertEqual(cv.status, 'approved')
74 |
--------------------------------------------------------------------------------
/tracker/tests/test_find_payout.py:
--------------------------------------------------------------------------------
1 | # from django.test import TestCase
2 | # from unittest import mock
3 | # from django.urls import reverse
4 | # from offer.models import Offer, Payout, Advertiser, Currency
5 | # from ..dao import find_payout
6 |
7 |
8 | # class TestFindPayout(TestCase):
9 |
10 | # def setUp(self):
11 | # super(TestFindPayout, self).setUp()
12 | # Offer.objects.create(
13 | # id=1,
14 | # title='blabla',
15 | # description='blabla',
16 | # advertiser=Advertiser.objects.create(
17 | # company='x',
18 | # email='x',
19 | # comment='x'
20 | # )
21 | # )
22 | # payout = Payout.objects.create(
23 | # offer_id=1,
24 | # revenue=0,
25 | # payout=0,
26 | # goal_value='2',
27 | # currency=Currency.objects.create(code='USD', name='Dollar')
28 | # )
29 | # payout.countries.add('RU')
30 |
31 | # def test_payout_found(self):
32 | # self.assertTrue(bool(find_payout(1, 'RU', '2')))
33 |
--------------------------------------------------------------------------------
/tracker/tests/test_postback_view.py:
--------------------------------------------------------------------------------
1 | from django.test import TestCase
2 | from unittest import mock
3 | from django.urls import reverse
4 |
5 |
6 | class TestPostbackHanlder(TestCase):
7 |
8 | def test_returns_400(self):
9 | url = reverse('tracker-postback')
10 | response = self.client.get(url)
11 | self.assertEqual(400, response.status_code)
12 |
13 | @mock.patch('tracker.views.conversion')
14 | def test_returns_200(self, mock_conversion):
15 | url = reverse('tracker-postback')
16 | response = self.client.get(url, {'click_id': 1, 'goal': 1})
17 | self.assertEqual(200, response.status_code)
18 | self.assertTrue(mock_conversion.delay.called)
19 |
--------------------------------------------------------------------------------
/tracker/urls.py:
--------------------------------------------------------------------------------
1 | from django.urls import path
2 |
3 | from . import views
4 |
5 |
6 | urlpatterns = [
7 | path('click', views.click, name='tracker-click'),
8 | path('postback', views.postback, name='tracker-postback'),
9 | ]
10 |
--------------------------------------------------------------------------------
/tracker/views.py:
--------------------------------------------------------------------------------
1 | import uuid
2 | from typing import Dict, Any
3 | from django.shortcuts import redirect
4 | from django.http import HttpResponse, HttpRequest
5 | from tracker.tasks.click import click as click_task
6 | from tracker.tasks.conversion import conversion
7 | from tracker.dao import TrackerCache
8 | from .models import conversion_statuses
9 |
10 |
11 | # class ClickData(NamedTuple):
12 | # click_id: str
13 | # offer_id: int
14 | # pid: int
15 | # ip: str
16 | # ua: str
17 | # sub1: str
18 | # sub2: str
19 | # sub3: str
20 | # sub4: str
21 | # sub5: str
22 | # fb_id: str
23 |
24 |
25 | def get_client_ip(request: HttpRequest) -> str:
26 | x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
27 | if x_forwarded_for:
28 | ip = x_forwarded_for.split(',')[0]
29 | else:
30 | ip = request.META.get('REMOTE_ADDR')
31 | return ip
32 |
33 |
34 | def replace_macro(url: str, context: Dict[str, Any]) -> str:
35 | url = url.replace('{click_id}', context['click_id'])
36 | url = url.replace('{clickid}', context['click_id'])
37 | url = url.replace('{pid}', context['pid'])
38 | url = url.replace('{fb_id}', context['fb_id'])
39 | return url
40 |
41 |
42 | def click(request):
43 | offer_id = request.GET.get('offer_id')
44 | pid = request.GET.get('pid')
45 | if not offer_id or not pid:
46 | return HttpResponse("Missing parameters", status=400)
47 |
48 | offer_data = TrackerCache.get_offer(offer_id)
49 | if not offer_data:
50 | return HttpResponse(status=404)
51 |
52 | click_id = uuid.uuid4().hex
53 |
54 | data = {
55 | 'click_id': click_id,
56 | 'offer_id': offer_id,
57 | 'pid': pid,
58 | 'ip': get_client_ip(request),
59 | 'ua': request.META['HTTP_USER_AGENT'],
60 | 'sub1': request.GET.get('sub1', ""),
61 | 'sub2': request.GET.get('sub2', ""),
62 | 'sub3': request.GET.get('sub3', ""),
63 | 'sub4': request.GET.get('sub4', ""),
64 | 'sub5': request.GET.get('sub5', ""),
65 | }
66 |
67 | click_task.delay(data)
68 |
69 | context = {
70 | 'click_id': click_id,
71 | 'pid': pid,
72 | 'fb_id': request.GET.get('fb_id', ""),
73 | }
74 | url = replace_macro(offer_data['tracking_link'], context)
75 |
76 | return redirect(url)
77 |
78 |
79 | def postback(request):
80 | click_id = request.GET.get('click_id')
81 | goal = request.GET.get('goal', '1')
82 | status = request.GET.get('status')
83 |
84 | try:
85 | sum_ = float(request.GET.get('sum', ''))
86 | except ValueError:
87 | sum_ = 0.0
88 |
89 | if not click_id:
90 | resp = HttpResponse("Missing click_id")
91 | resp.status_code = 400
92 | return resp
93 |
94 | data = {
95 | 'click_id': click_id,
96 | 'goal': goal,
97 | 'sum': sum_,
98 | }
99 |
100 | available_status_codes = list(map(lambda t: t[0], conversion_statuses))
101 | if status in available_status_codes:
102 | data.update({'status': status})
103 |
104 | conversion.delay(data)
105 |
106 | return HttpResponse("Conversion logged")
107 |
--------------------------------------------------------------------------------
/user_profile/__init__.py:
--------------------------------------------------------------------------------
1 | default_app_config = 'user_profile.apps.UserProfileConfig'
2 |
--------------------------------------------------------------------------------
/user_profile/admin.py:
--------------------------------------------------------------------------------
1 | from django.contrib import admin
2 |
3 | from user_profile.models import Profile
4 |
5 | admin.site.register(Profile)
6 |
--------------------------------------------------------------------------------
/user_profile/apps.py:
--------------------------------------------------------------------------------
1 | from django.apps import AppConfig
2 |
3 |
4 | class UserProfileConfig(AppConfig):
5 | name = 'user_profile'
6 |
7 | def ready(self):
8 | super(UserProfileConfig, self).ready()
9 | import user_profile.signals # noqa: F401
10 |
--------------------------------------------------------------------------------
/user_profile/migrations/0001_initial.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 2.2.6 on 2020-06-17 21:48
2 |
3 | from django.conf import settings
4 | from django.db import migrations, models
5 | import django.db.models.deletion
6 |
7 |
8 | class Migration(migrations.Migration):
9 |
10 | initial = True
11 |
12 | dependencies = [
13 | migrations.swappable_dependency(settings.AUTH_USER_MODEL),
14 | ]
15 |
16 | operations = [
17 | migrations.CreateModel(
18 | name='Profile',
19 | fields=[
20 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
21 | ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
22 | ],
23 | ),
24 | ]
25 |
--------------------------------------------------------------------------------
/user_profile/migrations/0002_profile_manager.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 2.2.6 on 2020-06-17 21:55
2 |
3 | from django.conf import settings
4 | from django.db import migrations, models
5 | import django.db.models.deletion
6 |
7 |
8 | class Migration(migrations.Migration):
9 |
10 | dependencies = [
11 | migrations.swappable_dependency(settings.AUTH_USER_MODEL),
12 | ('user_profile', '0001_initial'),
13 | ]
14 |
15 | operations = [
16 | migrations.AddField(
17 | model_name='profile',
18 | name='manager',
19 | field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='affiliates', to=settings.AUTH_USER_MODEL),
20 | ),
21 | ]
22 |
--------------------------------------------------------------------------------
/user_profile/migrations/0003_auto_20200617_2157.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 2.2.6 on 2020-06-17 21:57
2 |
3 | from django.conf import settings
4 | from django.db import migrations, models
5 | import django.db.models.deletion
6 |
7 |
8 | class Migration(migrations.Migration):
9 |
10 | dependencies = [
11 | ('user_profile', '0002_profile_manager'),
12 | ]
13 |
14 | operations = [
15 | migrations.AlterField(
16 | model_name='profile',
17 | name='manager',
18 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='affiliates', to=settings.AUTH_USER_MODEL),
19 | ),
20 | ]
21 |
--------------------------------------------------------------------------------
/user_profile/migrations/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cpanova/cpa-network/1b5d1fbb3dfaa64441ee0be505b9eb75b44cb659/user_profile/migrations/__init__.py
--------------------------------------------------------------------------------
/user_profile/models.py:
--------------------------------------------------------------------------------
1 | from django.contrib.auth import get_user_model
2 | from django.db import models
3 |
4 | User = get_user_model()
5 |
6 |
7 | class Profile(models.Model):
8 | user = models.OneToOneField(User, on_delete=models.CASCADE)
9 |
10 | manager = models.ForeignKey(
11 | get_user_model(),
12 | related_name='affiliates',
13 | on_delete=models.SET_NULL,
14 | null=True,
15 | blank=True
16 | )
17 |
18 | def __str__(self):
19 | return self.user.username
20 |
--------------------------------------------------------------------------------
/user_profile/signals.py:
--------------------------------------------------------------------------------
1 | from django.db.models.signals import post_save
2 | from django.dispatch import receiver
3 |
4 | from user_profile.models import Profile, User
5 |
6 |
7 | @receiver(post_save, sender=User)
8 | def create_user_profile(sender, instance, created, **kwargs):
9 | if created:
10 | Profile.objects.create(user=instance)
11 |
--------------------------------------------------------------------------------
/user_profile/tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cpanova/cpa-network/1b5d1fbb3dfaa64441ee0be505b9eb75b44cb659/user_profile/tests/__init__.py
--------------------------------------------------------------------------------
/user_profile/tests/test_profile.py:
--------------------------------------------------------------------------------
1 | from rest_framework.test import APITestCase
2 |
3 | from user_profile.models import User
4 |
5 |
6 | class ProfileTestCase(APITestCase):
7 | def setUp(self):
8 | super(ProfileTestCase, self).setUp()
9 | credentials = {
10 | 'username': 'test@test.com',
11 | 'password': '1234',
12 | }
13 | self.user = User.objects.create_user(**credentials)
14 | self.user.first_name = 'test username'
15 | self.user.save()
16 | # self.user.profile.phone = '+7 123 456 7890'
17 | # self.user.profile.skype = '@test'
18 | # self.user.profile.save()
19 | self.client.login(**credentials)
20 |
21 | # def test_get_profile(self):
22 | # response = self.client.get('/api/profile/')
23 | # self.assertEqual(200, response.status_code)
24 | # # self.assertEqual(
25 | # # {
26 | # # 'first_name': 'test username',
27 | # # 'phone': '+7 123 456 7890',
28 | # # 'skype': '@test',
29 | # # },
30 | # # response.data
31 | # # )
32 |
33 | # def test_patch_profile(self):
34 | # response = self.client.patch('/api/profile/',
35 | # {'phone': '+1 123 456 7890'})
36 | # self.assertEqual(200, response.status_code)
37 | # # self.assertEqual(
38 | # # {
39 | # # 'first_name': 'test username',
40 | # # 'phone': '+1 123 456 7890',
41 | # # 'skype': '@test',
42 | # # },
43 | # # response.data
44 | # # )
45 |
46 | # def test_patch_first_name(self):
47 | # response = self.client.patch('/api/profile/',
48 | # {'first_name': 'test test'})
49 | # self.assertEqual(200, response.status_code)
50 | # self.assertEqual(
51 | # {
52 | # 'first_name': 'test test',
53 | # 'phone': '+7 123 456 7890',
54 | # 'skype': '@test',
55 | # },
56 | # response.data
57 | # )
58 | # self.assertEqual(
59 | # 'test test',
60 | # User.objects.get(pk=self.user.pk).first_name
61 | # )
62 |
--------------------------------------------------------------------------------
/user_profile/tests/test_signal.py:
--------------------------------------------------------------------------------
1 | from django.test import TestCase
2 |
3 | from user_profile.models import Profile, User
4 |
5 |
6 | class SignalTestCase(TestCase):
7 | def test_create_profile(self):
8 | user = User.objects.create_user(username='test@test.com')
9 | self.assertEqual(
10 | 1,
11 | Profile.objects.filter(user=user).count()
12 | )
13 |
--------------------------------------------------------------------------------